[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 16:11:13 +00:00
committed by Wyatt Johnson
parent 68839c721c
commit f4037ce6fb
43 changed files with 1047 additions and 110 deletions
@@ -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 {}}
/>
`;