[CORL-260] Bring back sorting (#2186)

* feat: sort stream

* feat: add FieldSet component to ui

* feat: make accessible and add feature test

* test: fix snapshots
This commit is contained in:
Kiwi
2019-02-13 17:11:13 +01:00
committed by Wyatt Johnson
parent 68839c721c
commit f4037ce6fb
43 changed files with 1047 additions and 110 deletions
@@ -3,6 +3,7 @@ import React, { StatelessComponent } from "react";
import { DURATION_UNIT, DurationField } from "talk-framework/components";
import {
FieldSet,
FormField,
HorizontalGutter,
InputLabel,
@@ -27,7 +28,7 @@ interface Props {
const ClosingCommentStreamsConfig: StatelessComponent<Props> = ({
disabled,
}) => (
<HorizontalGutter size="oneAndAHalf" container="fieldset">
<HorizontalGutter size="oneAndAHalf" container={<FieldSet />}>
<Localized id="configure-general-closingCommentStreams-title">
<Header container="legend">Closing Comment Streams</Header>
</Localized>
@@ -40,13 +41,13 @@ const ClosingCommentStreamsConfig: StatelessComponent<Props> = ({
storys publication
</Typography>
</Localized>
<FormField container="fieldset">
<FormField container={<FieldSet />}>
<Localized id="configure-general-closingCommentStreams-closeCommentsAutomatically">
<InputLabel container="legend">Close Comments Automatically</InputLabel>
</Localized>
<OnOffField name="autoCloseStream" disabled={disabled} />
</FormField>
<FormField container="fieldset">
<FormField container={<FieldSet />}>
<Localized id="configure-general-closingCommentStreams-closeCommentsAfter">
<InputLabel container="legend">Close Comments After</InputLabel>
</Localized>
@@ -9,6 +9,7 @@ import {
validateWholeNumberGreaterThanOrEqual,
} from "talk-framework/lib/validation";
import {
FieldSet,
FormField,
HorizontalGutter,
InputLabel,
@@ -38,7 +39,7 @@ const CommentEditingConfig: StatelessComponent<Props> = ({ disabled }) => (
</Typography>
</Localized>
<FormField container="fieldset">
<FormField container={<FieldSet />}>
<Localized id="configure-general-commentEditing-commentEditTimeFrame">
<InputLabel container="legend">Comment Edit Timeframe</InputLabel>
</Localized>
@@ -8,6 +8,7 @@ import {
validateWholeNumberGreaterThan,
} from "talk-framework/lib/validation";
import {
FieldSet,
FormField,
HorizontalGutter,
InputLabel,
@@ -38,7 +39,7 @@ interface Props {
}
const CommentLengthConfig: StatelessComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf" container="fieldset">
<HorizontalGutter size="oneAndAHalf" container={<FieldSet />}>
<Localized id="configure-general-commentLength-title">
<Header container="legend">Comment Length</Header>
</Localized>
@@ -4,6 +4,7 @@ import { Field } from "react-final-form";
import { ExternalLink } from "talk-framework/lib/i18n/components";
import {
FieldSet,
FormField,
HorizontalGutter,
InputLabel,
@@ -21,12 +22,12 @@ interface Props {
}
const GuidelinesConfig: StatelessComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf" container="fieldset">
<HorizontalGutter size="oneAndAHalf" container={<FieldSet />}>
<Localized id="configure-general-guidelines-title">
<Header container="legend">Community Guidelines Summary</Header>
</Localized>
<FormField container="fieldset">
<FormField container={<FieldSet />}>
<Localized id="configure-general-guidelines-showCommunityGuidelines">
<InputLabel container="legend">
Show Community Guidelines Summary
@@ -10,6 +10,7 @@ import {
Validator,
} from "talk-framework/lib/validation";
import {
FieldSet,
FormField,
HorizontalGutter,
InputLabel,
@@ -38,7 +39,7 @@ const AkismetConfig: StatelessComponent<Props> = ({ disabled }) => {
return "";
};
return (
<HorizontalGutter size="oneAndAHalf" container="fieldset">
<HorizontalGutter size="oneAndAHalf" container={<FieldSet />}>
<Localized id="configure-moderation-akismet-title">
<Header container="legend">Akismet Spam Detection Filter</Header>
</Localized>
@@ -57,7 +58,7 @@ const AkismetConfig: StatelessComponent<Props> = ({ disabled }) => {
</Typography>
</Localized>
<FormField container="fieldset">
<FormField container={<FieldSet />}>
<Localized id="configure-moderation-akismet-filter">
<InputLabel container="legend">Spam Detection Filter</InputLabel>
</Localized>
@@ -14,6 +14,7 @@ import {
Validator,
} from "talk-framework/lib/validation";
import {
FieldSet,
FormField,
HorizontalGutter,
InputDescription,
@@ -49,7 +50,7 @@ const PerspectiveConfig: StatelessComponent<Props> = ({ disabled }) => {
return "";
};
return (
<HorizontalGutter size="oneAndAHalf" container="fieldset">
<HorizontalGutter size="oneAndAHalf" container={<FieldSet />}>
<Localized id="configure-moderation-perspective-title">
<Header container="legend">Perspective Toxic Comment Filter</Header>
</Localized>
@@ -66,7 +67,7 @@ const PerspectiveConfig: StatelessComponent<Props> = ({ disabled }) => {
</Typography>
</Localized>
<FormField container="fieldset">
<FormField container={<FieldSet />}>
<Localized id="configure-moderation-perspective-filter">
<InputLabel container="legend">Spam Detection Filter</InputLabel>
</Localized>
@@ -126,7 +127,7 @@ const PerspectiveConfig: StatelessComponent<Props> = ({ disabled }) => {
</Field>
</FormField>
<FormField container="fieldset">
<FormField container={<FieldSet />}>
<Localized id="configure-moderation-perspective-allowStoreCommentData">
<InputLabel container="legend">
Allow Google to Store Comment Data
@@ -108,7 +108,7 @@ exports[`renders configure general 1`] = `
data-testid="configure-generalContainer"
>
<fieldset
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
className="FieldSet-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<legend
className="Typography-root Typography-heading1 Typography-colorTextPrimary Header-root"
@@ -116,7 +116,7 @@ exports[`renders configure general 1`] = `
Community Guidelines Summary
</legend>
<fieldset
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
className="FieldSet-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<legend
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
@@ -208,7 +208,7 @@ Markdown can be found
</div>
</fieldset>
<fieldset
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
className="FieldSet-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<legend
className="Typography-root Typography-heading1 Typography-colorTextPrimary Header-root"
@@ -358,7 +358,7 @@ Edited comments are marked as (Edited) on the comment stream and the
moderation panel.
</p>
<fieldset
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
className="FieldSet-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<legend
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
@@ -417,7 +417,7 @@ moderation panel.
</select>
<span
aria-hidden={true}
className="SelectField-iconWrapper"
className="SelectField-afterWrapper"
>
<span
aria-hidden="true"
@@ -431,7 +431,7 @@ moderation panel.
</fieldset>
</div>
<fieldset
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
className="FieldSet-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<legend
className="Typography-root Typography-heading1 Typography-colorTextPrimary Header-root"
@@ -444,7 +444,7 @@ moderation panel.
Set comment streams to close after a defined period of time after a storys publication
</p>
<fieldset
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
className="FieldSet-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<legend
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
@@ -499,7 +499,7 @@ moderation panel.
</div>
</fieldset>
<fieldset
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
className="FieldSet-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<legend
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
@@ -558,7 +558,7 @@ moderation panel.
</select>
<span
aria-hidden={true}
className="SelectField-iconWrapper"
className="SelectField-afterWrapper"
>
<span
aria-hidden="true"
@@ -108,7 +108,7 @@ exports[`renders configure moderation 1`] = `
data-testid="configure-moderationContainer"
>
<fieldset
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
className="FieldSet-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<legend
className="Typography-root Typography-heading1 Typography-colorTextPrimary Header-root"
@@ -131,7 +131,7 @@ the
. If approved by a moderator, the comment will be published.
</p>
<fieldset
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
className="FieldSet-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<legend
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
@@ -225,7 +225,7 @@ comment is toxic, according to Perspective API. By default the treshold is set t
</div>
</div>
<fieldset
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
className="FieldSet-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<legend
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
@@ -370,7 +370,7 @@ improve the API over time
</div>
</fieldset>
<fieldset
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
className="FieldSet-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<legend
className="Typography-root Typography-heading1 Typography-colorTextPrimary Header-root"
@@ -397,7 +397,7 @@ are placed in the
the comment will be published.
</p>
<fieldset
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
className="FieldSet-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<legend
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
@@ -19,6 +19,7 @@ exports[`init local state 1`] = `
\\"network\\": {
\\"__ref\\": \\"client:root.local.network\\"
},
\\"defaultStreamOrderBy\\": \\"CREATED_AT_DESC\\",
\\"authPopup\\": {
\\"__ref\\": \\"client:root.local.authPopup\\"
},
@@ -39,6 +39,8 @@ export default async function initLocalState(
if (query.commentID) {
localRecord.setValue(query.commentID, "commentID");
}
// Set sort
localRecord.setValue("CREATED_AT_DESC", "defaultStreamOrderBy");
// Create authPopup Record
const authPopupRecord = createAndRetain(
@@ -54,5 +56,8 @@ export default async function initLocalState(
// Set active tab
localRecord.setValue("COMMENTS", "activeTab");
// Set sort
localRecord.setValue("CREATED_AT_DESC", "defaultStreamOrderBy");
});
}
@@ -38,6 +38,7 @@ type Local {
storyID: String
storyURL: String
commentID: String
defaultStreamOrderBy: COMMENT_SORT!
}
extend type Query {
@@ -0,0 +1,21 @@
import { Environment, RecordSource } from "relay-runtime";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import { commit } from "./SetStreamOrderByMutation";
let environment: Environment;
const source: RecordSource = new RecordSource();
beforeAll(() => {
environment = createRelayEnvironment({
source,
});
});
it("Sets streamOrderBy", () => {
const orderBy = "CREATED_AT_ASC";
commit(environment, { orderBy });
expect(source.get(LOCAL_ID)!.streamOrderBy).toEqual(orderBy);
});
@@ -0,0 +1,31 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { createMutationContainer, LOCAL_ID } from "talk-framework/lib/relay";
export interface SetStreamOrderByInput {
orderBy:
| "CREATED_AT_ASC"
| "CREATED_AT_DESC"
| "REPLIES_DESC"
| "RESPECT_DESC"
| "%future added value";
}
export type SetStreamOrderByMutation = (
input: SetStreamOrderByInput
) => Promise<void>;
export async function commit(
environment: Environment,
input: SetStreamOrderByInput
) {
return commitLocalUpdate(environment, store => {
const record = store.get(LOCAL_ID)!;
record.setValue(input.orderBy, "streamOrderBy");
});
}
export const withSetStreamOrderByMutation = createMutationContainer(
"setStreamOrderBy",
commit
);
@@ -0,0 +1,5 @@
.root {
border: 0;
border-top: 1px solid var(--palette-divider);
margin: var(--spacing-unit) 0;
}
@@ -0,0 +1,6 @@
import React, { StatelessComponent } from "react";
import * as styles from "./Divider.css";
const Divider: StatelessComponent = () => <hr className={styles.root} />;
export default Divider;
@@ -0,0 +1,8 @@
.mobileSelect {
font-size: 0;
padding: 11px;
width: 0px;
}
.mobileAfterWrapper {
right: 5px;
}
@@ -0,0 +1,53 @@
import { noop } from "lodash";
import React from "react";
import TestRenderer from "react-test-renderer";
import { LocalizationProvider } from "fluent-react/compat";
import { PropTypesOf } from "talk-framework/types";
import { UIContext, UIContextProps } from "talk-ui/components";
import SortMenu from "./SortMenu";
it("renders correctly on small screens", () => {
const props: PropTypesOf<typeof SortMenu> = {
orderBy: "CREATED_AT_ASC",
onChange: noop,
};
const context: UIContextProps = {
mediaQueryValues: {
width: 320,
},
};
const testRenderer = TestRenderer.create(
<LocalizationProvider bundles={[]}>
<UIContext.Provider value={context}>
<SortMenu {...props} />
</UIContext.Provider>
</LocalizationProvider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("renders correctly on big screens", () => {
const props: PropTypesOf<typeof SortMenu> = {
orderBy: "CREATED_AT_ASC",
onChange: noop,
};
const context: UIContextProps = {
mediaQueryValues: {
width: 1600,
},
};
const testRenderer = TestRenderer.create(
<LocalizationProvider bundles={[]}>
<UIContext.Provider value={context}>
<SortMenu {...props} />
</UIContext.Provider>
</LocalizationProvider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,70 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import {
Flex,
Icon,
MatchMedia,
Option,
SelectField,
Typography,
} from "talk-ui/components";
import Divider from "./Divider";
import * as styles from "./SortMenu.css";
interface Props {
orderBy:
| "CREATED_AT_ASC"
| "CREATED_AT_DESC"
| "REPLIES_DESC"
| "RESPECT_DESC"
| "%future added value";
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
}
const SortMenu: StatelessComponent<Props> = props => (
<MatchMedia ltWidth="sm">
{matches => (
<div>
<Flex justifyContent="flex-end" alignItems="center" itemGutter>
{!matches && (
<Localized id="comments-sortMenu-sortBy">
<Typography
variant="bodyCopyBold"
container={<label htmlFor="talk-comments-sortMenu" />}
>
Sort By
</Typography>
</Localized>
)}
<SelectField
id="talk-comments-sortMenu"
value={props.orderBy}
onChange={props.onChange}
afterWrapper={(matches && <Icon>sort</Icon>) || undefined}
classes={{
select: (matches && styles.mobileSelect) || undefined,
afterWrapper: (matches && styles.mobileAfterWrapper) || undefined,
}}
>
<Localized id="comments-sortMenu-newest">
<Option value="CREATED_AT_DESC">Newest</Option>
</Localized>
<Localized id="comments-sortMenu-oldest">
<Option value="CREATED_AT_ASC">Oldest</Option>
</Localized>
<Localized id="comments-sortMenu-mostReplies">
<Option value="REPLIES_DESC">Most Replies</Option>
</Localized>
<Localized id="comments-sortMenu-mostReactions">
<Option value="RESPECT_DESC">Most Reactions</Option>
</Localized>
</SelectField>
</Flex>
<Divider />
</div>
)}
</MatchMedia>
);
export default SortMenu;
@@ -27,6 +27,8 @@ it("renders correctly", () => {
disableLoadMore: false,
hasMore: false,
me: null,
orderBy: "CREATED_AT_ASC",
onChangeOrderBy: noop,
};
const wrapper = shallow(<StreamN {...props} />);
expect(wrapper).toMatchSnapshot();
@@ -50,6 +52,8 @@ describe("when use is logged in", () => {
label: "Respect",
},
},
orderBy: "CREATED_AT_ASC",
onChangeOrderBy: noop,
};
const wrapper = shallow(<StreamN {...props} />);
expect(wrapper).toMatchSnapshot();
@@ -73,6 +77,8 @@ describe("when there is more", () => {
disableLoadMore: false,
hasMore: true,
me: null,
orderBy: "CREATED_AT_ASC",
onChangeOrderBy: noop,
};
const wrapper = shallow(<StreamN {...props} />);
@@ -4,12 +4,13 @@ import { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import UserBoxContainer from "talk-stream/containers/UserBoxContainer";
import { Button, HorizontalGutter } from "talk-ui/components";
import { Button, HorizontalGutter, Spinner } from "talk-ui/components";
import CommentContainer from "../containers/CommentContainer";
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
import ReplyListContainer from "../containers/ReplyListContainer";
import PostCommentFormFake from "./PostCommentFormFake";
import SortMenu from "./SortMenu";
import styles from "./Stream.css";
@@ -34,6 +35,9 @@ export interface StreamProps {
PropTypesOf<typeof CommentContainer>["me"] &
PropTypesOf<typeof ReplyListContainer>["me"]
| null;
orderBy: PropTypesOf<typeof SortMenu>["orderBy"];
onChangeOrderBy: (e: React.ChangeEvent<HTMLSelectElement>) => void;
refetching?: boolean;
}
const Stream: StatelessComponent<StreamProps> = props => {
@@ -47,43 +51,49 @@ const Stream: StatelessComponent<StreamProps> = props => {
<PostCommentFormFake />
)}
</HorizontalGutter>
<HorizontalGutter
id="talk-comments-stream-log"
data-testid="comments-stream-log"
role="log"
aria-live="polite"
>
{props.comments.map(comment => (
<HorizontalGutter key={comment.id}>
<CommentContainer
me={props.me}
settings={props.settings}
comment={comment}
story={props.story}
/>
<ReplyListContainer
settings={props.settings}
me={props.me}
comment={comment}
story={props.story}
/>
</HorizontalGutter>
))}
{props.hasMore && (
<Localized id="comments-stream-loadMore">
<Button
id={"talk-comments-stream-loadMore"}
onClick={props.onLoadMore}
variant="outlined"
fullWidth
disabled={props.disableLoadMore}
aria-controls="talk-comments-stream-log"
>
Load More
</Button>
</Localized>
)}
</HorizontalGutter>
{props.comments.length > 0 && (
<SortMenu orderBy={props.orderBy} onChange={props.onChangeOrderBy} />
)}
{props.refetching && <Spinner />}
{!props.refetching && (
<HorizontalGutter
id="talk-comments-stream-log"
data-testid="comments-stream-log"
role="log"
aria-live="polite"
>
{props.comments.map(comment => (
<HorizontalGutter key={comment.id}>
<CommentContainer
me={props.me}
settings={props.settings}
comment={comment}
story={props.story}
/>
<ReplyListContainer
settings={props.settings}
me={props.me}
comment={comment}
story={props.story}
/>
</HorizontalGutter>
))}
{props.hasMore && (
<Localized id="comments-stream-loadMore">
<Button
id={"talk-comments-stream-loadMore"}
onClick={props.onLoadMore}
variant="outlined"
fullWidth
disabled={props.disableLoadMore}
aria-controls="talk-comments-stream-log"
>
Load More
</Button>
</Localized>
)}
</HorizontalGutter>
)}
</HorizontalGutter>
);
};
@@ -0,0 +1,119 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly on big screens 1`] = `
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-justifyFlexEnd Flex-alignCenter"
>
<label
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary"
htmlFor="talk-comments-sortMenu"
>
Sort By
</label>
<span
className="SelectField-root"
>
<select
className="SelectField-select undefined"
id="talk-comments-sortMenu"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
value="CREATED_AT_ASC"
>
<option
value="CREATED_AT_DESC"
>
Newest
</option>
<option
value="CREATED_AT_ASC"
>
Oldest
</option>
<option
value="REPLIES_DESC"
>
Most Replies
</option>
<option
value="RESPECT_DESC"
>
Most Reactions
</option>
</select>
<span
aria-hidden={true}
className="SelectField-afterWrapper undefined"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
expand_more
</span>
</span>
</span>
</div>
<hr
className="Divider-root"
/>
</div>
`;
exports[`renders correctly on small screens 1`] = `
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-justifyFlexEnd Flex-alignCenter"
>
<span
className="SelectField-root"
>
<select
className="SelectField-select SortMenu-mobileSelect"
id="talk-comments-sortMenu"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
value="CREATED_AT_ASC"
>
<option
value="CREATED_AT_DESC"
>
Newest
</option>
<option
value="CREATED_AT_ASC"
>
Oldest
</option>
<option
value="REPLIES_DESC"
>
Most Replies
</option>
<option
value="RESPECT_DESC"
>
Most Reactions
</option>
</select>
<span
aria-hidden={true}
className="SelectField-afterWrapper SortMenu-mobileAfterWrapper"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
sort
</span>
</span>
</span>
</div>
<hr
className="Divider-root"
/>
</div>
`;
@@ -21,6 +21,10 @@ exports[`renders correctly 1`] = `
/>
<PostCommentFormFake />
</ForwardRef(forwardRef)>
<SortMenu
onChange={[Function]}
orderBy="CREATED_AT_ASC"
/>
<ForwardRef(forwardRef)
aria-live="polite"
data-testid="comments-stream-log"
@@ -148,6 +152,10 @@ exports[`when there is more disables load more button 1`] = `
/>
<PostCommentFormFake />
</ForwardRef(forwardRef)>
<SortMenu
onChange={[Function]}
orderBy="CREATED_AT_ASC"
/>
<ForwardRef(forwardRef)
aria-live="polite"
data-testid="comments-stream-log"
@@ -289,6 +297,10 @@ exports[`when there is more renders a load more button 1`] = `
/>
<PostCommentFormFake />
</ForwardRef(forwardRef)>
<SortMenu
onChange={[Function]}
orderBy="CREATED_AT_ASC"
/>
<ForwardRef(forwardRef)
aria-live="polite"
data-testid="comments-stream-log"
@@ -432,6 +444,10 @@ exports[`when use is logged in renders correctly 1`] = `
storyID="story-id"
/>
</ForwardRef(forwardRef)>
<SortMenu
onChange={[Function]}
orderBy="CREATED_AT_ASC"
/>
<ForwardRef(forwardRef)
aria-live="polite"
data-testid="comments-stream-log"
@@ -31,6 +31,7 @@ it("renders correctly", () => {
hasMore: noop,
isLoading: noop,
} as any,
defaultOrderBy: "CREATED_AT_ASC",
};
const wrapper = shallow(<StreamContainerN {...props} />);
expect(wrapper).toMatchSnapshot();
@@ -58,6 +59,7 @@ describe("when has more comments", () => {
isLoading: () => false,
loadMore: (_: any, callback: () => void) => (finishLoading = callback),
} as any,
defaultOrderBy: "CREATED_AT_ASC",
};
let wrapper: ShallowWrapper;
@@ -1,4 +1,4 @@
import React from "react";
import React, { ChangeEvent } from "react";
import { graphql, RelayPaginationProp } from "react-relay";
import { withPaginationContainer } from "talk-framework/lib/relay";
@@ -18,6 +18,7 @@ interface InnerProps {
settings: SettingsData;
me: MeData | null;
relay: RelayPaginationProp;
defaultOrderBy: COMMENT_SORT;
}
// tslint:disable-next-line:no-unused-expression
@@ -32,6 +33,27 @@ graphql`
export class StreamContainer extends React.Component<InnerProps> {
public state = {
disableLoadMore: false,
refetching: false,
};
private orderBy = this.props.defaultOrderBy;
private handleOnChangeOrderBy = (e: ChangeEvent<HTMLSelectElement>) => {
this.orderBy = e.target.value as COMMENT_SORT;
this.setState({ refetching: true });
this.props.relay.refetchConnection(
5,
err => {
if (err) {
// tslint:disable-next-line:no-console
console.error(err);
return;
}
this.setState({ refetching: false });
},
{
orderBy: e.target.value as COMMENT_SORT,
}
);
};
public render() {
@@ -45,6 +67,9 @@ export class StreamContainer extends React.Component<InnerProps> {
hasMore={this.props.relay.hasMore()}
disableLoadMore={this.state.disableLoadMore}
me={this.props.me}
orderBy={this.orderBy}
onChangeOrderBy={this.handleOnChangeOrderBy}
refetching={this.state.refetching}
/>
);
}
@@ -14,7 +14,10 @@ exports[`renders correctly 1`] = `
}
disableLoadMore={false}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"reaction": Object {
@@ -61,7 +64,10 @@ exports[`when has more comments renders hasMore 1`] = `
disableLoadMore={false}
hasMore={true}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"reaction": Object {
@@ -108,7 +114,10 @@ exports[`when has more comments when loading more disables load more button 1`]
disableLoadMore={true}
hasMore={true}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"reaction": Object {
@@ -155,7 +164,10 @@ exports[`when has more comments when loading more enable load more button after
disableLoadMore={false}
hasMore={true}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"reaction": Object {
@@ -11,7 +11,7 @@ it("renders stream container", () => {
error: null,
};
const renderer = createRenderer();
renderer.render(React.createElement(() => render(data)));
renderer.render(React.createElement(() => render(data, "CREATED_AT_ASC")));
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -21,7 +21,7 @@ it("renders loading", () => {
error: null,
};
const renderer = createRenderer();
renderer.render(React.createElement(() => render(data)));
renderer.render(React.createElement(() => render(data, "CREATED_AT_ASC")));
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -31,6 +31,6 @@ it("renders error", () => {
error: new Error("error"),
};
const renderer = createRenderer();
renderer.render(React.createElement(() => render(data)));
renderer.render(React.createElement(() => render(data, "CREATED_AT_ASC")));
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -11,20 +11,20 @@ import { StreamQueryLocal as Local } from "talk-stream/__generated__/StreamQuery
import { Delay, Spinner } from "talk-ui/components";
import StreamContainer from "../containers/StreamContainer";
interface InnerProps {
interface Props {
local: Local;
}
export const render = ({
error,
props,
}: ReadyState<QueryTypes["response"]>) => {
if (error) {
return <div>{error.message}</div>;
export const render = (
data: ReadyState<QueryTypes["response"]>,
defaultStreamOrderBy: Props["local"]["defaultStreamOrderBy"]
) => {
if (data.error) {
return <div>{data.error.message}</div>;
}
if (props) {
if (!props.story) {
if (data.props) {
if (!data.props.story) {
return (
<Localized id="comments-streamQuery-storyNotFound">
<div>Story not found</div>
@@ -33,9 +33,10 @@ export const render = ({
}
return (
<StreamContainer
settings={props.settings}
me={props.me}
story={props.story}
settings={data.props.settings}
me={data.props.me}
story={data.props.story}
defaultOrderBy={defaultStreamOrderBy}
/>
);
}
@@ -47,36 +48,45 @@ export const render = ({
);
};
const StreamQuery: StatelessComponent<InnerProps> = ({
local: { storyID, storyURL },
}) => (
<QueryRenderer<QueryTypes>
query={graphql`
query StreamQuery($storyID: ID, $storyURL: String) {
me {
...StreamContainer_me
const StreamQuery: StatelessComponent<Props> = props => {
const {
local: { storyID, storyURL, defaultStreamOrderBy },
} = props;
return (
<QueryRenderer<QueryTypes>
query={graphql`
query StreamQuery(
$storyID: ID
$storyURL: String
$streamOrderBy: COMMENT_SORT
) {
me {
...StreamContainer_me
}
story(id: $storyID, url: $storyURL) {
...StreamContainer_story @arguments(orderBy: $streamOrderBy)
}
settings {
...StreamContainer_settings
}
}
story(id: $storyID, url: $storyURL) {
...StreamContainer_story
}
settings {
...StreamContainer_settings
}
}
`}
variables={{
storyID,
storyURL,
}}
render={render}
/>
);
`}
variables={{
storyID,
storyURL,
streamOrderBy: defaultStreamOrderBy,
}}
render={data => render(data, props.local.defaultStreamOrderBy)}
/>
);
};
const enhanced = withLocalStateContainer(
graphql`
fragment StreamQueryLocal on Local {
storyID
storyURL
defaultStreamOrderBy
}
`
)(StreamQuery);
@@ -16,6 +16,7 @@ exports[`renders loading 1`] = `
exports[`renders stream container 1`] = `
<Relay(StreamContainer)
defaultOrderBy="CREATED_AT_ASC"
story={Object {}}
/>
`;
@@ -171,6 +171,65 @@ exports[`renders app with comment stream 1`] = `
</button>
</div>
</div>
<div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-justifyFlexEnd Flex-alignCenter"
>
<label
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary"
htmlFor="talk-comments-sortMenu"
>
Sort By
</label>
<span
className="SelectField-root"
>
<select
className="SelectField-select undefined"
id="talk-comments-sortMenu"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
value="CREATED_AT_DESC"
>
<option
value="CREATED_AT_DESC"
>
Newest
</option>
<option
value="CREATED_AT_ASC"
>
Oldest
</option>
<option
value="REPLIES_DESC"
>
Most Replies
</option>
<option
value="RESPECT_DESC"
>
Most Reactions
</option>
</select>
<span
aria-hidden={true}
className="SelectField-afterWrapper undefined"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
expand_more
</span>
</span>
</span>
</div>
<hr
className="Divider-root"
/>
</div>
<div
aria-live="polite"
className="HorizontalGutter-root HorizontalGutter-full"
@@ -0,0 +1,310 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders app with comment stream 1`] = `
<div
aria-live="polite"
className="HorizontalGutter-root HorizontalGutter-full"
data-testid="comments-stream-log"
id="talk-comments-stream-log"
role="log"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<div
data-testid="comment-comment-2"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Indent-root"
>
<div
className=""
>
<div
className="Comment-root"
role="article"
>
<div
className="Flex-root Flex-flex Flex-justifySpaceBetween Flex-directionRow"
>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignBaseline Flex-directionRow"
>
<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>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Hey!",
}
}
/>
<div
className="Flex-root Flex-flex Flex-justifySpaceBetween"
>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-directionRow"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
id="comments-commentContainer-replyButton-comment-2"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Reply
</span>
</button>
<div
className="Popover-root"
>
<button
aria-controls="permalink-popover-comment-2"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Share
</span>
</button>
<div
aria-hidden={true}
aria-labelledby="permalink-popover-comment-2-ariainfo"
id="permalink-popover-comment-2"
role="popup"
>
<div
className="AriaInfo-root"
id="permalink-popover-comment-2-ariainfo"
>
A dialog showing a permalink to the comment
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Respect
</span>
</button>
</div>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-directionRow"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Report
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<div
data-testid="comment-comment-3"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Indent-root"
>
<div
className=""
>
<div
className="Comment-root"
role="article"
>
<div
className="Flex-root Flex-flex Flex-justifySpaceBetween Flex-directionRow"
>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
>
Isabelle
</span>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignBaseline Flex-directionRow"
>
<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>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Comment Body 3",
}
}
/>
<div
className="Flex-root Flex-flex Flex-justifySpaceBetween"
>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-directionRow"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
id="comments-commentContainer-replyButton-comment-3"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Reply
</span>
</button>
<div
className="Popover-root"
>
<button
aria-controls="permalink-popover-comment-3"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Share
</span>
</button>
<div
aria-hidden={true}
aria-labelledby="permalink-popover-comment-3-ariainfo"
id="permalink-popover-comment-3"
role="popup"
>
<div
className="AriaInfo-root"
id="permalink-popover-comment-3-ariainfo"
>
A dialog showing a permalink to the comment
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Respect
</span>
</button>
</div>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-directionRow"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Report
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -8,6 +8,7 @@ export default function create(params: CreateParams) {
localRecord.setValue("COMMENTS", "activeTab");
localRecord.setValue(false, "loggedIn");
localRecord.setValue("jti", "accessTokenJTI");
localRecord.setValue("CREATED_AT_DESC", "defaultStreamOrderBy");
params.initLocalState(localRecord, source, environment);
}
},
@@ -0,0 +1,81 @@
import sinon from "sinon";
import {
createSinonStub,
waitForElement,
within,
} from "talk-framework/testHelpers";
import { settings, stories } from "../fixtures";
import create from "./create";
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
settings: sinon.stub().returns(settings),
},
};
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(stories[0].id, "storyID");
},
});
return {
testRenderer,
};
};
it("renders app with comment stream", async () => {
const commentsQueryStub = createSinonStub(
s =>
s.onFirstCall().callsFake((input: any) => {
expect(input).toEqual({ first: 5, orderBy: "CREATED_AT_DESC" });
return stories[0].comments;
}),
s =>
s.onSecondCall().callsFake((input: any) => {
expect(input).toEqual({
after: null,
first: 5,
orderBy: "CREATED_AT_ASC",
});
return stories[1].comments;
})
);
const storyQueryStub = createSinonStub(s =>
s.callsFake((_: any, input: any) => {
expect(input.id).toEqual("story-1");
expect(input.url).toBeFalsy();
return {
...stories[0],
comments: commentsQueryStub,
};
})
);
const { testRenderer } = await createTestRenderer({
Query: {
story: storyQueryStub,
},
});
let streamLog = await waitForElement(() =>
within(testRenderer.root).getByTestID("comments-stream-log")
);
const selectField = within(testRenderer.root).getByLabelText("Sort By");
const oldestOption = within(selectField).getByText("Oldest");
selectField.props.onChange({
target: { value: oldestOption.props.value.toString() },
});
streamLog = await waitForElement(() =>
within(testRenderer.root).getByTestID("comments-stream-log")
);
expect(within(streamLog).toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,4 @@
.root {
border: 0;
padding: 0;
}
@@ -0,0 +1,10 @@
---
name: AriaInfo
menu: UI Kit
---
import { Playground, PropsTable } from "docz";
# FieldSet
Simple `fieldset` with removed styling for accessibility purposes.
@@ -0,0 +1,14 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { PropTypesOf } from "talk-framework/types";
import FieldSet from "./FieldSet";
it("renders correctly", () => {
const props: PropTypesOf<typeof FieldSet> = {
children: "content",
};
const renderer = TestRenderer.create(<FieldSet {...props} />);
expect(renderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,26 @@
import cn from "classnames";
import React, { AllHTMLAttributes, Ref, StatelessComponent } from "react";
import { withForwardRef, withStyles } from "talk-ui/hocs";
import { PropTypesOf } from "talk-ui/types";
import styles from "./FieldSet.css";
interface InnerProps extends AllHTMLAttributes<HTMLElement> {
/**
* This prop can be used to add custom classnames.
* It is handled by the `withStyles `HOC.
*/
classes: typeof styles;
/** Internal: Forwarded Ref */
forwardRef?: Ref<HTMLFieldSetElement>;
}
const FieldSet: StatelessComponent<InnerProps> = props => {
const { className, classes, forwardRef: ref, ...rest } = props;
const rootClassName = cn(classes.root, className);
return <fieldset className={rootClassName} {...rest} ref={ref} />;
};
const enhanced = withForwardRef(withStyles(styles)(FieldSet));
export type FieldSetProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<fieldset
className="FieldSet-root"
>
content
</fieldset>
`;
@@ -0,0 +1 @@
export { default } from "./FieldSet";
@@ -3,7 +3,7 @@
display: inline-block;
}
.iconWrapper {
.afterWrapper {
position: absolute;
display: inline-flex;
justify-content: center;
@@ -60,6 +60,6 @@
width: 100%;
}
.iconWrapperDisabled {
.afterWrapperDisabled {
color: var(--palette-text-secondary);
}
@@ -38,6 +38,8 @@ export interface SelectFieldProps {
onFocus: EventHandler<FocusEvent<HTMLSelectElement>>;
onBlur: EventHandler<FocusEvent<HTMLSelectElement>>;
keyboardFocus: boolean;
afterWrapper?: React.ReactElement<any>;
}
const SelectField: StatelessComponent<SelectFieldProps> = props => {
@@ -48,6 +50,7 @@ const SelectField: StatelessComponent<SelectFieldProps> = props => {
keyboardFocus,
children,
disabled,
afterWrapper,
...rest
} = props;
@@ -56,8 +59,8 @@ const SelectField: StatelessComponent<SelectFieldProps> = props => {
[classes.keyboardFocus]: keyboardFocus,
});
const iconWrapperClassName = cn(classes.iconWrapper, {
[classes.iconWrapperDisabled]: disabled,
const afterWrapperClassName = cn(classes.afterWrapper, {
[classes.afterWrapperDisabled]: disabled,
});
return (
@@ -65,12 +68,16 @@ const SelectField: StatelessComponent<SelectFieldProps> = props => {
<select className={selectClassName} disabled={disabled} {...rest}>
{children}
</select>
<span className={iconWrapperClassName} aria-hidden>
<Icon>expand_more</Icon>
<span className={afterWrapperClassName} aria-hidden>
{afterWrapper}
</span>
</span>
);
};
SelectField.defaultProps = {
afterWrapper: <Icon>expand_more</Icon>,
};
const enhanced = withStyles(styles)(withKeyboardFocus(SelectField));
export default enhanced;
@@ -17,7 +17,7 @@ exports[`renders correctly 1`] = `
/>
<span
aria-hidden={true}
className="SelectField-iconWrapper SelectField-iconWrapperDisabled"
className="SelectField-afterWrapper SelectField-afterWrapperDisabled"
>
<span
aria-hidden="true"
+1
View File
@@ -17,6 +17,7 @@ export { default as Popup } from "./Popup";
export { default as FormField } from "./FormField";
export { default as InputDescription } from "./InputDescription";
export { default as Spinner } from "./Spinner";
export { default as FieldSet } from "./FieldSet";
export { default as HorizontalGutter } from "./HorizontalGutter";
export { default as Icon } from "./Icon";
export { default as AriaInfo } from "./AriaInfo";
+6
View File
@@ -85,6 +85,12 @@ comments-replyTo = Replying to: <username></username>
comments-reportButton-report = Report
comments-reportButton-reported = Reported
comments-sortMenu-sortBy = Sort By
comments-sortMenu-newest = Newest
comments-sortMenu-oldest = Oldest
comments-sortMenu-mostReplies = Most Replies
comments-sortMenu-mostReactions = Most Reactions
## Profile Tab
profile-historyComment-viewConversation = View Conversation
profile-historyComment-replies = Replies {$replyCount}