[CORL-294] Moderate a single story + quick search (#2286)

* feat: allow passing a `storyID` to `Query.moderationQueues`

* feat: moderate by story

* feat: implement search story combobox

* feat: add translations

* fix: tests

* fix: duplicate id

* fix: rename file

* chore: add more comments

* fix: add missing translation

* review: use query parameter "q" instead of url path

* chore: move placeholder logic inside, maybe this makes it clearer :-D
This commit is contained in:
Kiwi
2019-04-26 14:23:46 +00:00
committed by Wyatt Johnson
parent a91de05af9
commit ab938985e4
86 changed files with 1934 additions and 319 deletions
@@ -3,10 +3,7 @@
}
.keyboardFocus {
outline-width: 3px;
outline-color: Highlight;
outline-color: -webkit-focus-ring-color;
outline-style: auto;
@mixin outline;
}
.mouseHover {
@@ -40,10 +40,7 @@
/* Box focus */
.label.focus:before {
outline-width: 3px;
outline-color: Highlight;
outline-color: -webkit-focus-ring-color;
outline-style: auto;
@mixin outline;
}
/* Box checked */
@@ -7,6 +7,7 @@ import { withStyles } from "talk-ui/hocs";
import AriaInfo from "../AriaInfo";
import { PropTypesOf } from "talk-ui/types";
import styles from "./Popover.css";
type Placement =
@@ -40,15 +41,19 @@ interface ChildrenRenderProps {
interface PopoverProps {
body: (props: BodyRenderProps) => React.ReactNode | React.ReactElement<any>;
children: (props: ChildrenRenderProps) => React.ReactNode;
description: string;
description?: string;
id: string;
className?: string;
placement?: Placement;
visible?: boolean;
classes: typeof styles;
modifiers?: PropTypesOf<typeof Popper>["modifiers"];
eventsEnabled?: PropTypesOf<typeof Popper>["eventsEnabled"];
positionFixed?: PropTypesOf<typeof Popper>["positionFixed"];
}
interface State {
visible: false;
visible: boolean;
}
class Popover extends React.Component<PopoverProps> {
@@ -109,10 +114,15 @@ class Popover extends React.Component<PopoverProps> {
className,
placement,
classes,
visible: controlledVisible,
positionFixed,
modifiers,
eventsEnabled,
...rest
} = this.props;
const { visible } = this.state;
const visible =
controlledVisible !== undefined ? controlledVisible : this.state.visible;
const popoverClassName = cn(classes.popover, {
[classes.top]: placement!.startsWith("top"),
[classes.left]: placement!.startsWith("left"),
@@ -128,11 +138,16 @@ class Popover extends React.Component<PopoverProps> {
children({
ref: props.ref,
toggleVisibility: this.toggleVisibility,
visible: this.state.visible,
visible,
})
}
</Reference>
<Popper placement={placement} eventsEnabled positionFixed={false}>
<Popper
placement={placement}
eventsEnabled={eventsEnabled}
positionFixed={positionFixed}
modifiers={modifiers}
>
{props => (
<div
id={id}
@@ -140,7 +155,9 @@ class Popover extends React.Component<PopoverProps> {
aria-labelledby={`${id}-ariainfo`}
aria-hidden={!visible}
>
<AriaInfo id={`${id}-ariainfo`}>{description}</AriaInfo>
{description && (
<AriaInfo id={`${id}-ariainfo`}>{description}</AriaInfo>
)}
{visible && (
<div
style={props.style}
@@ -151,7 +168,7 @@ class Popover extends React.Component<PopoverProps> {
? body({
scheduleUpdate: props.scheduleUpdate,
toggleVisibility: this.toggleVisibility,
visible: this.state.visible,
visible,
})
: body}
</div>
@@ -35,10 +35,7 @@
/* Box focus */
.label.focus:before {
outline-width: 3px;
outline-color: Highlight;
outline-color: -webkit-focus-ring-color;
outline-style: auto;
@mixin outline;
}
/* Box checked */
@@ -15,10 +15,7 @@
}
.keyboardFocus:focus {
outline-width: 3px;
outline-color: Highlight;
outline-color: -webkit-focus-ring-color;
outline-style: auto;
@mixin outline;
}
.select {
@@ -4,7 +4,7 @@ import { withStyles } from "talk-ui/hocs";
import styles from "./Spinner.css";
type Size = "sm" | "md";
type Size = "xs" | "sm" | "md";
export interface SpinnerProps {
/**
@@ -21,6 +21,8 @@ export interface SpinnerProps {
function calculateSize(size: Size): number {
switch (size) {
case "xs":
return 15;
case "sm":
return 30;
case "md":
@@ -1,12 +1,12 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import React, { HTMLAttributes, StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import { withStyles } from "talk-ui/hocs";
import styles from "./SubBar.css";
interface Props {
interface Props extends HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
className?: string;
gutterBegin?: boolean;
@@ -7,13 +7,11 @@
}
.header {
composes: bodyCopy from "talk-ui/shared/typography.css";
color: var(--palette-grey-dark);
composes: tableHeading from "talk-ui/shared/typography.css";
}
.body {
composes: detail from "talk-ui/shared/typography.css";
color: var(--palette-grey-dark);
composes: tableData from "talk-ui/shared/typography.css";
}
.alignCenter {
+1
View File
@@ -1,5 +1,6 @@
export { default as BaseButton } from "./BaseButton";
export { default as Button } from "./Button";
export { default as Backdrop } from "./Backdrop";
export { default as ButtonIcon } from "./Button/ButtonIcon";
export { default as Typography } from "./Typography";
export { default as Popover } from "./Popover";
+6
View File
@@ -0,0 +1,6 @@
/** blur removes focus from current active element */
export default function blur() {
if (window.document.activeElement) {
(window.document.activeElement as any).blur();
}
}
@@ -0,0 +1,35 @@
export default function combineEventHandlers<A, B, C, D>(
a: A,
b: B,
c: C,
d: D
): A & B & C & D;
export default function combineEventHandlers<A, B, C>(
a: A,
b: B,
c: C
): A & B & C;
export default function combineEventHandlers<A, B>(a: A, b: B): A & B;
/**
* combineEventHandlers expects prop objects with React
* like onEvent handlers e.g. onClick, onKeydown and combine them
* by calling them one after another.
*/
export default function combineEventHandlers(...propObjects: any[]): any {
const result: any = {};
propObjects.forEach(o => {
Object.keys(o).forEach(k => {
if (k in result) {
const prev = result[k];
result[k] = (...args: any[]) => {
prev(...args);
(o as any)[k](...args);
};
} else {
result[k] = (o as any)[k];
}
});
});
return result;
}
+2
View File
@@ -0,0 +1,2 @@
export { default as blur } from "./blur";
export { default as combineEventHandlers } from "./combineEventHandlers";
+4
View File
@@ -0,0 +1,4 @@
export { default as useFocus } from "./useFocus";
export { default as usePreventFocusLoss } from "./usePreventFocusLoss";
export { default as useBlurOnEsc } from "./useBlurOnEsc";
export { default as useComboBox } from "./useComboBox";
+17
View File
@@ -0,0 +1,17 @@
import { useCallback } from "react";
import { blur } from "../helpers";
/** useBlurOnEsc returns handlers that calls blur when pressing ESC */
export default function useBlurOnEsc(active: boolean) {
return {
onKeyDown: useCallback(
(evt: React.KeyboardEvent) => {
if (evt.keyCode === 27) {
blur();
}
},
[active]
),
};
}
+114
View File
@@ -0,0 +1,114 @@
import React, { useState } from "react";
/**
* ActiveDescendant is the id of the current active selection, which needs to be passed to `aria-activedescendant` prop
*/
export type ActiveDescendant = string | undefined;
/** EventHandlers are the ones that should be passed to the TextField */
export interface EventHandlers {
onBlur: React.EventHandler<React.FocusEvent>;
onChange: React.EventHandler<React.FormEvent<HTMLInputElement>>;
onKeyDown: React.EventHandler<React.KeyboardEvent>;
}
export type ListBoxOptionElement = React.ReactElement<{
id: string;
"aria-selected": boolean;
onClick: React.EventHandler<React.MouseEvent>;
href?: string;
}>;
/**
* ListBoxOptionClickOrEnterHandler is called whenever a click or enter
* occurs on a list box option
*/
export type ListBoxOptionClickOrEnterHandler = (
evt: React.KeyboardEvent | React.MouseEvent,
element: ListBoxOptionElement
) => void;
/**
* ListBoxOption is the data for the options to pass to `useComboBox`.
*/
export interface ListBoxOption {
element: ListBoxOptionElement;
onClickOrEnter?: ListBoxOptionClickOrEnterHandler;
}
/**
* useComboBox accepts an `id` and a list of `ListBoxOption` and returns
* a managed list of `ListBoxOption`, the active descendant and event handlers
* for the textfield. You can find the managed props of the elements in
* the type `ListBoxOptionElements`.
*
* @param id unique identifier for accessibility purposes
* @param opts options to show in the listbox
*/
export default function useComboBox<T extends ListBoxOption>(
id: string,
opts: T[]
): [T[], ActiveDescendant, EventHandlers] {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
return [
opts.map((item, i) => ({
...item,
element: React.cloneElement(item.element, {
id: `${id}-${i}`,
"aria-selected": activeIndex === i,
key: `${id}-${i}`,
onClick:
item.element.props.onClick ||
((evt: React.MouseEvent) => {
if (item.onClickOrEnter) {
item.onClickOrEnter!(evt, item.element);
}
}),
}),
})),
activeIndex !== null ? `${id}-${activeIndex}` : undefined,
{
onBlur: () => {
/** Reset active selection when the textfield is blurred */
setActiveIndex(null);
},
onChange: () => {
/** Reset active selection when the text changes */
setActiveIndex(null);
},
onKeyDown: (evt: React.KeyboardEvent) => {
// On Arror Down
if (evt.keyCode === 40) {
if (activeIndex !== opts.length - 1) {
if (activeIndex === null) {
setActiveIndex(0);
} else {
setActiveIndex((activeIndex || 0) + 1);
}
}
evt.preventDefault();
}
// On Arrow Up
else if (evt.keyCode === 38) {
if (activeIndex !== 0) {
if (activeIndex === null) {
setActiveIndex(0);
} else {
setActiveIndex((activeIndex || 0) - 1);
}
}
evt.preventDefault();
}
// On ENTER
else if (evt.keyCode === 13) {
if (activeIndex !== null && opts[activeIndex].onClickOrEnter) {
opts[activeIndex].onClickOrEnter!(evt, opts[activeIndex].element);
}
} else {
// Any other key.
setActiveIndex(null);
}
},
},
];
}
+12
View File
@@ -0,0 +1,12 @@
import { EventHandler, FocusEvent, useCallback, useState } from "react";
/** useFocus tracks the focus state and returns the event handlers to do so */
export default function useFocus(): [
boolean,
{ onFocus: EventHandler<FocusEvent>; onBlur: EventHandler<FocusEvent> }
] {
const [focused, setFocused] = useState<boolean>(false);
const onFocus = useCallback(() => setFocused(true), [setFocused]);
const onBlur = useCallback(() => setFocused(false), [setFocused]);
return [focused, { onFocus, onBlur }];
}
@@ -0,0 +1,16 @@
import { useCallback } from "react";
/** usePreventFocusLoss returns event handlers that will prevent the current focus from being lost */
export default function usePreventFocusLoss(active: boolean) {
return {
onMouseDown: useCallback(
(evt: React.MouseEvent) => {
if (!active || evt.target === window.document.activeElement) {
return;
}
evt.preventDefault();
},
[active]
),
};
}
+16
View File
@@ -260,3 +260,19 @@
letter-spacing: calc(0.1em / 18);
color: var(--palette-text-primary);
}
.tableHeading {
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-medium);
font-size: calc(14rem / var(--rem-base));
line-height: calc(20em / 14);
color: var(--palette-grey-dark);
}
.tableData {
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-regular);
font-size: calc(14rem / var(--rem-base));
line-height: calc(14em / 14);
color: var(--palette-grey-dark);
}
+6
View File
@@ -94,3 +94,9 @@
letter-spacing: calc(0.2em / 16);
color: var(--palette-grey-darkest);
}
@define-mixin outline {
outline-width: 3px;
outline-color: Highlight;
outline-color: -webkit-focus-ring-color;
outline-style: auto;
}