diff --git a/config/watcher.ts b/config/watcher.ts index 277ec34ec..c91874c9b 100644 --- a/config/watcher.ts +++ b/config/watcher.ts @@ -4,6 +4,8 @@ import { Config, LongRunningExecutor, } from "../scripts/watcher"; +// Ensure environment variables are read. +import "./env"; const config: Config = { rootDir: path.resolve(__dirname, "../src"), diff --git a/doczrc.js b/doczrc.js index 9d7ef1b12..8f78be5b9 100644 --- a/doczrc.js +++ b/doczrc.js @@ -25,6 +25,10 @@ export default { options: { modules: true, importLoaders: 1, + localIdentName: + process.env.NODE_ENV === "production" + ? "[hash:base64]" + : "[name]-[local]-[hash:base64:5]", }, }, { diff --git a/package-lock.json b/package-lock.json index 271d5e135..4d50f11b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14501,6 +14501,12 @@ "css-mediaquery": "^0.1.2" } }, + "material-design-icons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz", + "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=", + "dev": true + }, "math-expression-evaluator": { "version": "1.2.17", "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", @@ -21985,6 +21991,18 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typeface-manuale": { + "version": "0.0.54", + "resolved": "https://registry.npmjs.org/typeface-manuale/-/typeface-manuale-0.0.54.tgz", + "integrity": "sha512-wWgWFPSvoxEDdm8Dmq0cZzgmBfRXho5WhxU6x4dKBk7WIBsyvRgcjT+j7cQ3ah/9VYp1+nJzQWhCue+TiqA7FA==", + "dev": true + }, + "typeface-source-sans-pro": { + "version": "0.0.54", + "resolved": "https://registry.npmjs.org/typeface-source-sans-pro/-/typeface-source-sans-pro-0.0.54.tgz", + "integrity": "sha512-4nsvldyZsOBuvR4oEuUMTquJFVssP4iIVnMEIU4paCWo2940b6r/t95sk7KwDpCablS8DprM/YlhpgqnS7gpKg==", + "dev": true + }, "typescript": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", diff --git a/package.json b/package.json index 5f709a7e1..2cc25ebd6 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "jest-junit": "^5.1.0", "jsdom": "^11.11.0", "loader-utils": "^1.1.0", + "material-design-icons": "^3.0.1", "npm-run-all": "^4.1.3", "postcss-advanced-variables": "^2.3.3", "postcss-css-variables": "^0.9.0", @@ -172,6 +173,8 @@ "tslint-plugin-prettier": "^1.3.0", "tslint-react": "^3.6.0", "typed-css-modules": "^0.3.4", + "typeface-manuale": "0.0.54", + "typeface-source-sans-pro": "0.0.54", "typescript": "^2.9.2", "uglifyjs-webpack-plugin": "^1.2.5", "webpack": "4.12.0", diff --git a/src/core/client/framework/lib/relay/createMutationContainer.tsx b/src/core/client/framework/lib/relay/createMutationContainer.tsx index d0a91e92d..0446e7d17 100644 --- a/src/core/client/framework/lib/relay/createMutationContainer.tsx +++ b/src/core/client/framework/lib/relay/createMutationContainer.tsx @@ -22,10 +22,10 @@ function createMutationContainer( ): InferableComponentEnhancer<{ [P in T]: (input: I) => Promise }> { return compose( withContext(({ relayEnvironment }) => ({ relayEnvironment })), - hoistStatics((WrappedComponent: React.ComponentType) => { + hoistStatics((BaseComponent: React.ComponentType) => { class CreateMutationContainer extends React.Component { public static displayName = wrapDisplayName( - WrappedComponent, + BaseComponent, "createMutationContainer" ); @@ -38,7 +38,7 @@ function createMutationContainer( const inject = { [propName]: this.commit, }; - return ; + return ; } } return CreateMutationContainer as React.ComponentType; diff --git a/src/core/client/framework/lib/relay/withLocalStateContainer.tsx b/src/core/client/framework/lib/relay/withLocalStateContainer.tsx index 9de4180bb..9eaeb0ce8 100644 --- a/src/core/client/framework/lib/relay/withLocalStateContainer.tsx +++ b/src/core/client/framework/lib/relay/withLocalStateContainer.tsx @@ -34,7 +34,7 @@ function withLocalStateContainer( ): InferableComponentEnhancer<{ local: T }> { return compose( withContext(({ relayEnvironment }) => ({ relayEnvironment })), - hoistStatics((WrappedComponent: React.ComponentType) => { + hoistStatics((BaseComponent: React.ComponentType) => { class LocalStateContainer extends React.Component { constructor(props: Props) { super(props); @@ -65,7 +65,7 @@ function withLocalStateContainer( public render() { const { relayEnvironment: _, ...rest } = this.props; - return ; + return ; } } return LocalStateContainer as React.ComponentType; diff --git a/src/core/client/framework/types.ts b/src/core/client/framework/types.ts index 6ea1faf15..60a609441 100644 --- a/src/core/client/framework/types.ts +++ b/src/core/client/framework/types.ts @@ -1,2 +1,2 @@ // TODO: (@cvle) Extract useful common types into its own package. -export { Diff, Omit, Overwrite, PropTypesOf } from "talk-ui/types"; +export { Omit, Overwrite, PropTypesOf } from "talk-ui/types"; diff --git a/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap index 9253404bd..02f3c7726 100644 --- a/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap +++ b/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`renders correctly on big screens 1`] = `
Hello World diff --git a/src/core/client/stream/components/Comment/__snapshots__/Username.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/Username.spec.tsx.snap index ab0e141e6..54224f9b0 100644 --- a/src/core/client/stream/components/Comment/__snapshots__/Username.spec.tsx.snap +++ b/src/core/client/stream/components/Comment/__snapshots__/Username.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`renders correctly on big screens 1`] = ` Marvin @@ -10,7 +10,7 @@ exports[`renders correctly on big screens 1`] = ` exports[`renders correctly on small screens 1`] = ` Marvin diff --git a/src/core/client/stream/components/Indent.css b/src/core/client/stream/components/Indent.css index c675b03a6..d779d93e0 100644 --- a/src/core/client/stream/components/Indent.css +++ b/src/core/client/stream/components/Indent.css @@ -4,5 +4,5 @@ } .level0 { - border-color: var(--palette-secondary-darkest); + border-color: var(--palette-grey-darkest); } diff --git a/src/core/client/stream/components/PostCommentForm.css b/src/core/client/stream/components/PostCommentForm.css index df045285d..7e3ef2e15 100644 --- a/src/core/client/stream/components/PostCommentForm.css +++ b/src/core/client/stream/components/PostCommentForm.css @@ -1,5 +1,5 @@ .textarea { - composes: body1 from "talk-ui/shared/typography.css"; + composes: bodyCopy from "talk-ui/shared/typography.css"; display: block; height: 100px; diff --git a/src/core/client/stream/components/PostCommentForm.tsx b/src/core/client/stream/components/PostCommentForm.tsx index 09db70e39..badcbaca8 100644 --- a/src/core/client/stream/components/PostCommentForm.tsx +++ b/src/core/client/stream/components/PostCommentForm.tsx @@ -41,7 +41,7 @@ const PostCommentForm: StatelessComponent = props => (
- diff --git a/src/core/client/stream/components/ReplyList.spec.tsx b/src/core/client/stream/components/ReplyList.spec.tsx index 093d96049..76750c8a0 100644 --- a/src/core/client/stream/components/ReplyList.spec.tsx +++ b/src/core/client/stream/components/ReplyList.spec.tsx @@ -39,4 +39,11 @@ describe("when there is more", () => { .simulate("click"); expect((props.onShowAll as SinonSpy).calledOnce).toBe(true); }); + + const wrapperDisabledButton = shallow( + + ); + it("disables load more button", () => { + expect(wrapperDisabledButton).toMatchSnapshot(); + }); }); diff --git a/src/core/client/stream/components/ReplyList.tsx b/src/core/client/stream/components/ReplyList.tsx index 17ad32ed2..6f4380769 100644 --- a/src/core/client/stream/components/ReplyList.tsx +++ b/src/core/client/stream/components/ReplyList.tsx @@ -34,8 +34,7 @@ const ReplyList: StatelessComponent = props => { aria-controls={`talk-comments-replyList-log--${props.commentID}`} onClick={props.onShowAll} disabled={props.disableShowAll} - secondary - invert + variant="outlined" fullWidth > Show All Replies diff --git a/src/core/client/stream/components/Stream.tsx b/src/core/client/stream/components/Stream.tsx index da55050f5..9dbb21c5b 100644 --- a/src/core/client/stream/components/Stream.tsx +++ b/src/core/client/stream/components/Stream.tsx @@ -42,8 +42,7 @@ const Stream: StatelessComponent = props => {
`; @@ -71,14 +71,14 @@ exports[`when there is more disables load more button 1`] = ` - - - - + - + @@ -126,14 +126,13 @@ exports[`when there is more disables load more button 1`] = ` disabled={true} fullWidth={true} id="talk-comments-stream-loadMore" - invert={true} onClick={[Function]} - secondary={true} + variant="outlined" > Load More - +
`; @@ -147,14 +146,14 @@ exports[`when there is more renders a load more button 1`] = ` - - - - + - + @@ -202,13 +201,12 @@ exports[`when there is more renders a load more button 1`] = ` disabled={false} fullWidth={true} id="talk-comments-stream-loadMore" - invert={true} onClick={[Function]} - secondary={true} + variant="outlined" > Load More - +
`; diff --git a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap index b3d193c41..065e376b5 100644 --- a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap @@ -2,13 +2,13 @@ exports[`loads more comments 1`] = `

Talk NEO

@@ -28,11 +28,14 @@ exports[`loads more comments 1`] = ` className="PostCommentForm-postButtonContainer" > @@ -54,7 +57,7 @@ exports[`loads more comments 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Markus @@ -67,7 +70,7 @@ exports[`loads more comments 1`] = `

Joining Too

@@ -83,7 +86,7 @@ exports[`loads more comments 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Lukas @@ -96,7 +99,7 @@ exports[`loads more comments 1`] = `

What's up?

@@ -112,7 +115,7 @@ exports[`loads more comments 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Isabelle @@ -125,7 +128,7 @@ exports[`loads more comments 1`] = `

Hey!

@@ -138,13 +141,13 @@ exports[`loads more comments 1`] = ` exports[`renders comment stream 1`] = `

Talk NEO

@@ -164,11 +167,14 @@ exports[`renders comment stream 1`] = ` className="PostCommentForm-postButtonContainer" > @@ -190,7 +196,7 @@ exports[`renders comment stream 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Markus @@ -203,7 +209,7 @@ exports[`renders comment stream 1`] = `

Joining Too

@@ -219,7 +225,7 @@ exports[`renders comment stream 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Lukas @@ -232,7 +238,7 @@ exports[`renders comment stream 1`] = `

What's up?

@@ -240,13 +246,16 @@ exports[`renders comment stream 1`] = ` diff --git a/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap index 24fe4fe1d..290be7545 100644 --- a/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap @@ -2,13 +2,13 @@ exports[`renders comment stream 1`] = `

Talk NEO

@@ -28,11 +28,14 @@ exports[`renders comment stream 1`] = ` className="PostCommentForm-postButtonContainer" > @@ -54,7 +57,7 @@ exports[`renders comment stream 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Markus @@ -67,7 +70,7 @@ exports[`renders comment stream 1`] = `

Joining Too

@@ -83,7 +86,7 @@ exports[`renders comment stream 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Markus @@ -96,7 +99,7 @@ exports[`renders comment stream 1`] = `

I like yoghurt

@@ -116,7 +119,7 @@ exports[`renders comment stream 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Markus @@ -129,7 +132,7 @@ exports[`renders comment stream 1`] = `

Joining Too

@@ -141,7 +144,7 @@ exports[`renders comment stream 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Lukas @@ -154,7 +157,7 @@ exports[`renders comment stream 1`] = `

What's up?

diff --git a/src/core/client/stream/test/__snapshots__/renderStream.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/renderStream.spec.tsx.snap index 3a3599547..0290e7f21 100644 --- a/src/core/client/stream/test/__snapshots__/renderStream.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/renderStream.spec.tsx.snap @@ -2,13 +2,13 @@ exports[`renders comment stream 1`] = `

Talk NEO

@@ -28,11 +28,14 @@ exports[`renders comment stream 1`] = ` className="PostCommentForm-postButtonContainer" > @@ -54,7 +57,7 @@ exports[`renders comment stream 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Markus @@ -67,7 +70,7 @@ exports[`renders comment stream 1`] = `

Joining Too

@@ -83,7 +86,7 @@ exports[`renders comment stream 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Lukas @@ -96,7 +99,7 @@ exports[`renders comment stream 1`] = `

What's up?

diff --git a/src/core/client/stream/test/__snapshots__/showAllReplies.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/showAllReplies.spec.tsx.snap index 2762b7dae..3fcf31abb 100644 --- a/src/core/client/stream/test/__snapshots__/showAllReplies.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/showAllReplies.spec.tsx.snap @@ -2,13 +2,13 @@ exports[`renders comment stream 1`] = `

Talk NEO

@@ -28,11 +28,14 @@ exports[`renders comment stream 1`] = ` className="PostCommentForm-postButtonContainer" > @@ -54,7 +57,7 @@ exports[`renders comment stream 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Markus @@ -67,7 +70,7 @@ exports[`renders comment stream 1`] = `

Joining Too

@@ -87,7 +90,7 @@ exports[`renders comment stream 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Lukas @@ -100,20 +103,23 @@ exports[`renders comment stream 1`] = `

What's up?

@@ -127,13 +133,13 @@ exports[`renders comment stream 1`] = ` exports[`show all replies 1`] = `

Talk NEO

@@ -153,11 +159,14 @@ exports[`show all replies 1`] = ` className="PostCommentForm-postButtonContainer" > @@ -179,7 +188,7 @@ exports[`show all replies 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Markus @@ -192,7 +201,7 @@ exports[`show all replies 1`] = `

Joining Too

@@ -212,7 +221,7 @@ exports[`show all replies 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Lukas @@ -225,7 +234,7 @@ exports[`show all replies 1`] = `

What's up?

@@ -237,7 +246,7 @@ exports[`show all replies 1`] = ` className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn" > Isabelle @@ -250,7 +259,7 @@ exports[`show all replies 1`] = `

Hey!

diff --git a/src/core/client/ui/components/BaseButton/BaseButton.css b/src/core/client/ui/components/BaseButton/BaseButton.css index be2f0fa20..14b111258 100644 --- a/src/core/client/ui/components/BaseButton/BaseButton.css +++ b/src/core/client/ui/components/BaseButton/BaseButton.css @@ -8,3 +8,6 @@ outline-color: -webkit-focus-ring-color; outline-style: auto; } + +.mouseHover { +} diff --git a/src/core/client/ui/components/BaseButton/BaseButton.spec.tsx b/src/core/client/ui/components/BaseButton/BaseButton.spec.tsx new file mode 100644 index 000000000..b7754c865 --- /dev/null +++ b/src/core/client/ui/components/BaseButton/BaseButton.spec.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; + +import { PropTypesOf } from "talk-framework/types"; + +import BaseButton from "./BaseButton"; + +it("renders correctly", () => { + const props: PropTypesOf = { + className: "my-class", + children: "Push Me", + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders as anchor", () => { + const props: PropTypesOf = { + anchor: true, + children: "Push Me", + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/BaseButton/BaseButton.tsx b/src/core/client/ui/components/BaseButton/BaseButton.tsx index 1d97a9d0c..28399c1af 100644 --- a/src/core/client/ui/components/BaseButton/BaseButton.tsx +++ b/src/core/client/ui/components/BaseButton/BaseButton.tsx @@ -1,8 +1,13 @@ import cn from "classnames"; -import React from "react"; +import React, { Ref } from "react"; import { ButtonHTMLAttributes, StatelessComponent } from "react"; -import { withKeyboardFocus, withStyles } from "talk-ui/hocs"; +import { + withForwardRef, + withKeyboardFocus, + withMouseHover, + withStyles, +} from "talk-ui/hocs"; import { PropTypesOf } from "talk-ui/types"; import * as styles from "./BaseButton.css"; @@ -10,7 +15,6 @@ import * as styles from "./BaseButton.css"; interface InnerProps extends ButtonHTMLAttributes { /** If set renders an anchor tag instead */ anchor?: boolean; - /** * This prop can be used to add custom classnames. * It is handled by the `withStyles `HOC. @@ -18,7 +22,13 @@ interface InnerProps extends ButtonHTMLAttributes { classes: typeof styles; /** This is passed by the `withKeyboardFocus` HOC */ - keyboardFocus: boolean; + keyboardFocus?: boolean; + + /** This is passed by the `withMouseHover` HOC */ + mouseHover?: boolean; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; } /** @@ -30,6 +40,8 @@ const BaseButton: StatelessComponent = ({ className, classes, keyboardFocus, + mouseHover, + forwardRef, type: typeProp, ...rest }) => { @@ -51,11 +63,14 @@ const BaseButton: StatelessComponent = ({ const rootClassName = cn(classes.root, className, { [classes.keyboardFocus]: keyboardFocus, + [classes.mouseHover]: mouseHover, }); - return ; + return ; }; -const enhanced = withStyles(styles)(withKeyboardFocus(BaseButton)); +const enhanced = withForwardRef( + withStyles(styles)(withMouseHover(withKeyboardFocus(BaseButton))) +); export type BaseButtonProps = PropTypesOf; export default enhanced; diff --git a/src/core/client/ui/components/BaseButton/__snapshots__/BaseButton.spec.tsx.snap b/src/core/client/ui/components/BaseButton/__snapshots__/BaseButton.spec.tsx.snap new file mode 100644 index 000000000..c6fd97a78 --- /dev/null +++ b/src/core/client/ui/components/BaseButton/__snapshots__/BaseButton.spec.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders as anchor 1`] = ` + + Push Me + +`; + +exports[`renders correctly 1`] = ` + +`; diff --git a/src/core/client/ui/components/BaseButton/index.ts b/src/core/client/ui/components/BaseButton/index.ts index 3c9664e0a..53e4924d5 100644 --- a/src/core/client/ui/components/BaseButton/index.ts +++ b/src/core/client/ui/components/BaseButton/index.ts @@ -1,2 +1 @@ -export * from "./BaseButton"; -export { default } from "./BaseButton"; +export { default, BaseButtonProps } from "./BaseButton"; diff --git a/src/core/client/ui/components/Button/Button.css b/src/core/client/ui/components/Button/Button.css index 17283a819..e3cab49f2 100644 --- a/src/core/client/ui/components/Button/Button.css +++ b/src/core/client/ui/components/Button/Button.css @@ -1,99 +1,282 @@ .root { - composes: button from "talk-ui/shared/typography.css"; + border-radius: var(--round-corners); + background-color: transparent; + position: relative; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border: 1px solid transparent; - &:enabled, - &:disabled { - padding: 5px 20px; - border-radius: var(--round-corners); - background-color: transparent; - /* TODO: hover styles for the default button */ + & > * { + margin: 0 calc(0.5 * var(--spacing-unit)) 0 0; + /* convince safari */ + align-self: center; + } + & > *:last-child { + margin: 0; } } -.root:disabled { +.root.disabled { opacity: 0.4; cursor: default; } -.fullWidth:enabled, -.fullWidth:disabled { +.fullWidth { display: block; width: 100%; box-sizing: border-box; } -.primary:enabled, -.primary:disabled { - background-color: var(--palette-primary-main); - color: #fff; +.sizeSmall { + composes: button from "talk-ui/shared/typography.css"; + padding: 4px 10px; } -.primary:enabled { - &:hover { - background-color: var(--palette-primary-light); +.sizeRegular { + composes: button from "talk-ui/shared/typography.css"; + padding: 8px 15px; +} + +.sizeLarge { + composes: buttonLarge from "talk-ui/shared/typography.css"; + padding: 10px 15px; +} + +.variantRegular { + &.colorRegular { + color: var(--palette-grey-main); + } + &.colorPrimary { + color: var(--palette-primary-main); + } + &.colorError { + color: var(--palette-error-main); + } + &.colorSuccess { + color: var(--palette-success-main); } - &:active { - background-color: var(--palette-primary-lighter); + &:not(.disabled) { + &.colorRegular { + &.mouseHover { + color: var(--palette-grey-light); + } + &:active, + &.active { + color: var(--palette-grey-lighter); + } + } + &.colorPrimary { + &.mouseHover { + color: var(--palette-primary-light); + } + &:active, + &.active { + color: var(--palette-primary-lighter); + } + } + &.colorError { + &.mouseHover { + color: var(--palette-error-light); + } + &:active, + &.active { + color: var(--palette-error-lighter); + } + } + &.colorSuccess { + &.mouseHover { + color: var(--palette-success-light); + } + &:active, + &.active { + color: var(--palette-success-lighter); + } + } } } -.primary:enabled.invert, -.primary:disabled.invert { - background-color: transparent; - border: 1px solid var(--palette-primary-main); - color: var(--palette-primary-main); -} - -.primary:enabled.invert { - &:hover { - border-color: var(--palette-primary-light); - color: var(--palette-primary-light); +.variantFilled { + color: var(--palette-common-white); + &.colorRegular { + background-color: var(--palette-grey-main); + } + &.colorPrimary { + background-color: var(--palette-primary-main); + } + &.colorError { + background-color: var(--palette-error-main); + } + &.colorSuccess { + background-color: var(--palette-success-main); } - &:active { - border-color: var(--palette-primary-lighter); - color: var(--palette-primary-lighter); + &:not(.disabled) { + &.colorRegular { + &.mouseHover { + background-color: var(--palette-grey-light); + } + &:active, + &.active { + background-color: var(--palette-grey-lighter); + } + } + &.colorPrimary { + &.mouseHover { + background-color: var(--palette-primary-light); + } + &:active, + &.active { + background-color: var(--palette-primary-lighter); + } + } + &.colorError { + &.mouseHover { + background-color: var(--palette-error-light); + } + &:active, + &.active { + background-color: var(--palette-error-lighter); + } + } + &.colorSuccess { + &.mouseHover { + background-color: var(--palette-success-light); + } + &:active, + &.active { + background-color: var(--palette-success-lighter); + } + } } } -.secondary:enabled, -.secondary:disabled { - background-color: var(--palette-secondary-main); - color: #fff; -} - -.secondary:enabled { - &:hover { - background-color: var(--palette-secondary-light); +.variantOutlined { + &.colorRegular { + color: var(--palette-grey-main); + border: 1px solid currentColor; } - &:active { - background-color: var(--palette-secondary-lighter); + &.colorPrimary { + color: var(--palette-primary-main); + border: 1px solid currentColor; + } + &.colorError { + color: var(--palette-error-main); + border: 1px solid currentColor; + } + &.colorSuccess { + color: var(--palette-success-main); + border: 1px solid currentColor; + } + + &:not(.disabled) { + &.colorRegular { + &.mouseHover { + color: var(--palette-grey-light); + border: 1px solid currentColor; + } + &:active, + &.active { + color: var(--palette-grey-lighter); + border: 1px solid currentColor; + } + } + &.colorPrimary { + &.mouseHover { + color: var(--palette-primary-light); + border: 1px solid currentColor; + } + &:active, + &.active { + color: var(--palette-primary-lighter); + border: 1px solid currentColor; + } + } + &.colorError { + &.mouseHover { + color: var(--palette-error-light); + border: 1px solid currentColor; + } + &:active, + &.active { + color: var(--palette-error-lighter); + border: 1px solid currentColor; + } + } + &.colorSuccess { + &.mouseHover { + color: var(--palette-success-light); + border: 1px solid currentColor; + } + &:active, + &.active { + color: var(--palette-success-lighter); + border: 1px solid currentColor; + } + } } } -.secondary:enabled.invert, -.secondary:disabled.invert { - background-color: transparent; - border: 1px solid var(--palette-secondary-main); - color: var(--palette-secondary-main); -} - -.secondary:enabled.invert { - &:hover { - border-color: var(--palette-secondary-light); - color: var(--palette-secondary-light); +.variantGhost { + &.colorRegular { + color: var(--palette-grey-main); + } + &.colorPrimary { + color: var(--palette-primary-main); + } + &.colorError { + color: var(--palette-error-main); + } + &.colorSuccess { + color: var(--palette-success-main); } - &:active { - border-color: var(--palette-secondary-lighter); - color: var(--palette-secondary-lighter); + &:not(.disabled) { + &.colorRegular { + &.mouseHover { + border: 1px solid currentColor; + } + &:active, + &.active { + border: 1px solid currentColor; + color: var(--palette-common-white); + background-color: var(--palette-grey-main); + } + } + &.colorPrimary { + &.mouseHover { + border: 1px solid currentColor; + } + &:active, + &.active { + border: 1px solid currentColor; + color: var(--palette-common-white); + background-color: var(--palette-primary-main); + } + } + &.colorError { + &.mouseHover { + border: 1px solid currentColor; + } + &:active, + &.active { + border: 1px solid currentColor; + color: var(--palette-common-white); + background-color: var(--palette-error-main); + } + } + &.colorSuccess { + &.mouseHover { + border: 1px solid currentColor; + } + &:active, + &.active { + border: 1px solid currentColor; + color: var(--palette-common-white); + background-color: var(--palette-success-main); + } + } } } - -/** - * This seems to be the best way to target modern touch device browsers. - */ -@media (-moz-touch-enabled: 1), (pointer: coarse) { - /* TODO: Remove hover styles */ -} diff --git a/src/core/client/ui/components/Button/Button.mdx b/src/core/client/ui/components/Button/Button.mdx index 5be82c540..f6d85b6fc 100644 --- a/src/core/client/ui/components/Button/Button.mdx +++ b/src/core/client/ui/components/Button/Button.mdx @@ -5,23 +5,72 @@ menu: UI Kit import { Playground } from 'docz' import Button from './Button' +import ButtonIcon from './ButtonIcon' +import Icon from '../Icon' +import Flex from '../Flex' # Button -## Basic usage +## Regular Button - - - - - - - - - - - - - + + + + + + + + + + + + +## Filled Button + + + + + + + + + + + + + + + +## Outlined Button + + + + + + + + + + + + + + + +## Ghost Button + + + + + + + + + + + + + + diff --git a/src/core/client/ui/components/Button/Button.spec.tsx b/src/core/client/ui/components/Button/Button.spec.tsx new file mode 100644 index 000000000..98ddf9ca0 --- /dev/null +++ b/src/core/client/ui/components/Button/Button.spec.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import ShallowRenderer from "react-test-renderer/shallow"; + +import { PropTypesOf } from "talk-framework/types"; + +import { Button } from "./Button"; + +it("renders correctly", () => { + const props: PropTypesOf = { + classes: { + mouseHover: "mouseHover", + keyboardFocus: "keyboardFocus", + } as any, + children: "Push me", + }; + const renderer = ShallowRenderer.createRenderer(); + renderer.render( + + + + + +## Direction + + + + + + + diff --git a/src/core/client/ui/components/Flex/Flex.spec.tsx b/src/core/client/ui/components/Flex/Flex.spec.tsx index b774d9961..5c68f0de0 100644 --- a/src/core/client/ui/components/Flex/Flex.spec.tsx +++ b/src/core/client/ui/components/Flex/Flex.spec.tsx @@ -1,5 +1,5 @@ -import { shallow } from "enzyme"; import React from "react"; +import TestRenderer from "react-test-renderer"; import { PropTypesOf } from "talk-ui/types"; @@ -11,10 +11,58 @@ it("renders correctly", () => { alignItems: "center", direction: "row", }; - const wrapper = shallow( + const renderer = TestRenderer.create(
Hello World
); - expect(wrapper).toMatchSnapshot(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders with wrap", () => { + const props: PropTypesOf = { + wrap: true, + }; + const renderer = TestRenderer.create( + +
Hello World
+
+ ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders with wrap reverse", () => { + const props: PropTypesOf = { + wrap: "reverse", + }; + const renderer = TestRenderer.create( + +
Hello World
+
+ ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders with item gutter", () => { + const props: PropTypesOf = { + itemGutter: true, + }; + const renderer = TestRenderer.create( + +
Hello World
+
+ ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders with halfe item gutter", () => { + const props: PropTypesOf = { + itemGutter: "half", + }; + const renderer = TestRenderer.create( + +
Hello World
+
+ ); + expect(renderer.toJSON()).toMatchSnapshot(); }); diff --git a/src/core/client/ui/components/Flex/Flex.tsx b/src/core/client/ui/components/Flex/Flex.tsx index bfba06d05..240e13f9e 100644 --- a/src/core/client/ui/components/Flex/Flex.tsx +++ b/src/core/client/ui/components/Flex/Flex.tsx @@ -1,12 +1,19 @@ import cn from "classnames"; -import React from "react"; +import React, { Ref } from "react"; import { StatelessComponent } from "react"; import { pascalCase } from "talk-common/utils"; +import { withForwardRef, withStyles } from "talk-ui/hocs"; +import { PropTypesOf } from "talk-ui/types"; import * as styles from "./Flex.css"; interface InnerProps { + /** + * This prop can be used to add custom classnames. + * It is handled by the `withStyles `HOC. + */ + classes: typeof styles; id?: string; role?: string; justifyContent?: @@ -20,38 +27,58 @@ interface InnerProps { direction?: "row" | "column" | "row-reverse" | "column-reverse"; itemGutter?: boolean | "half"; className?: string; + wrap?: boolean | "reverse"; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; } const Flex: StatelessComponent = props => { const { + classes, className, justifyContent, alignItems, direction, itemGutter, + wrap, + forwardRef, ...rest } = props; + let alignItemsWithDefault = alignItems; + if (!direction || !direction.startsWith("column")) { + alignItemsWithDefault = "center"; + } + const classObject: Record = { - [styles.itemGutter]: itemGutter === true, - [styles.halfItemGutter]: itemGutter === "half", + [classes.itemGutter]: itemGutter === true, + [classes.halfItemGutter]: itemGutter === "half", + [classes.wrap]: wrap === true, + [classes.wrapReverse]: wrap === "reverse", }; if (justifyContent) { - classObject[(styles as any)[`justify${pascalCase(justifyContent)}`]] = true; + classObject[ + (classes as any)[`justify${pascalCase(justifyContent)}`] + ] = true; } - if (alignItems) { - classObject[(styles as any)[`align${pascalCase(alignItems)}`]] = true; + if (alignItemsWithDefault) { + classObject[ + (classes as any)[`align${pascalCase(alignItemsWithDefault)}`] + ] = true; } if (direction) { - classObject[(styles as any)[`direction${pascalCase(direction)}`]] = true; + classObject[(classes as any)[`direction${pascalCase(direction)}`]] = true; } - const classNames: string = cn(styles.root, className, classObject); + const classNames: string = cn(classes.root, className, classObject); - return
; + return
; }; -export default Flex; +const enhanced = withForwardRef(withStyles(styles)(Flex)); +export default enhanced; +export type FlexProps = PropTypesOf; diff --git a/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap b/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap index 90ef93c13..e6530fe18 100644 --- a/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap +++ b/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap @@ -9,3 +9,43 @@ exports[`renders correctly 1`] = `
`; + +exports[`renders with halfe item gutter 1`] = ` +
+
+ Hello World +
+
+`; + +exports[`renders with item gutter 1`] = ` +
+
+ Hello World +
+
+`; + +exports[`renders with wrap 1`] = ` +
+
+ Hello World +
+
+`; + +exports[`renders with wrap reverse 1`] = ` +
+
+ Hello World +
+
+`; diff --git a/src/core/client/ui/components/Flex/index.ts b/src/core/client/ui/components/Flex/index.ts index 27e09f37b..434836f6d 100644 --- a/src/core/client/ui/components/Flex/index.ts +++ b/src/core/client/ui/components/Flex/index.ts @@ -1,2 +1 @@ -export * from "./Flex"; -export { default } from "./Flex"; +export { default, FlexProps } from "./Flex"; diff --git a/src/core/client/ui/components/Icon/Icon.css b/src/core/client/ui/components/Icon/Icon.css new file mode 100644 index 000000000..37fb390d5 --- /dev/null +++ b/src/core/client/ui/components/Icon/Icon.css @@ -0,0 +1,54 @@ +@font-face { + font-family: "Material Icons"; + font-style: normal; + font-weight: 400; + src: local("Material Icons"), local("MaterialIcons-Regular"), + url(material-design-icons/iconfont/MaterialIcons-Regular.woff2) + format("woff2"), + url(material-design-icons/iconfont/MaterialIcons-Regular.woff) + format("woff"), + url(material-design-icons/iconfont/MaterialIcons-Regular.ttf) + format("truetype"); +} + +.root { + font-family: "Material Icons"; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + overflow: hidden; + vertical-align: middle; + display: inline-block; + letter-spacing: 0; + + /* Enable Ligatures */ + font-feature-settings: "liga"; + font-variant-ligatures: "discretionary-ligatures"; + + /* Support for Safari and Chrome. */ + text-rendering: optimizeLegibility; + + /* Better Font Rendering */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.sm { + font-size: 18px; + width: 18px; +} +.md { + font-size: 24px; + width: 24px; +} +.lg { + font-size: 36px; + width: 36px; +} +.xl { + font-size: 48px; + width: 48px; +} diff --git a/src/core/client/ui/components/Icon/Icon.mdx b/src/core/client/ui/components/Icon/Icon.mdx new file mode 100644 index 000000000..83428c28b --- /dev/null +++ b/src/core/client/ui/components/Icon/Icon.mdx @@ -0,0 +1,21 @@ +--- +name: Icon +menu: UI Kit +--- + +import { Playground, PropsTable } from 'docz' +import Icon from './Icon' + +# Icon + +Renders an icon. + +Checkout available icons https://material.io/tools/icons/ + +## Basic usage + + face + face + face + face + diff --git a/src/core/client/ui/components/Icon/Icon.spec.tsx b/src/core/client/ui/components/Icon/Icon.spec.tsx new file mode 100644 index 000000000..93cf68a66 --- /dev/null +++ b/src/core/client/ui/components/Icon/Icon.spec.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; + +import { PropTypesOf } from "talk-framework/types"; + +import Icon from "./Icon"; + +it("renders correctly", () => { + const props: PropTypesOf = { + children: "face", + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("renders correctly with specified size", () => { + const props: PropTypesOf = { + size: "lg", + children: "bookmark", + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/Icon/Icon.tsx b/src/core/client/ui/components/Icon/Icon.tsx new file mode 100644 index 000000000..3b8889fa1 --- /dev/null +++ b/src/core/client/ui/components/Icon/Icon.tsx @@ -0,0 +1,37 @@ +import cn from "classnames"; +import React, { HTMLAttributes, Ref, StatelessComponent } from "react"; + +import { withForwardRef, withStyles } from "talk-ui/hocs"; +import { PropTypesOf } from "talk-ui/types"; + +import * as styles from "./Icon.css"; + +interface InnerProps extends HTMLAttributes { + /** + * This prop can be used to add custom classnames. + * It is handled by the `withStyles `HOC. + */ + classes: typeof styles; + + size?: "sm" | "md" | "lg" | "xl"; + + /** The name of the icon to render */ + children: string; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; +} + +const Icon: StatelessComponent = props => { + const { classes, className, size, forwardRef, ...rest } = props; + const rootClassName = cn(classes.root, className, classes[size!]); + return ; +}; + +Icon.defaultProps = { + size: "sm", +}; + +const enhanced = withForwardRef(withStyles(styles)(Icon)); +export type IconProps = PropTypesOf; +export default enhanced; diff --git a/src/core/client/ui/components/Icon/__snapshots__/Icon.spec.tsx.snap b/src/core/client/ui/components/Icon/__snapshots__/Icon.spec.tsx.snap new file mode 100644 index 000000000..5e5a8c969 --- /dev/null +++ b/src/core/client/ui/components/Icon/__snapshots__/Icon.spec.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + face + +`; + +exports[`renders correctly with specified size 1`] = ` + + bookmark + +`; diff --git a/src/core/client/ui/components/Icon/index.ts b/src/core/client/ui/components/Icon/index.ts new file mode 100644 index 000000000..d3ba5da55 --- /dev/null +++ b/src/core/client/ui/components/Icon/index.ts @@ -0,0 +1 @@ +export { default, IconProps } from "./Icon"; diff --git a/src/core/client/ui/components/MatchMedia/MatchMedia.tsx b/src/core/client/ui/components/MatchMedia/MatchMedia.tsx index 43d900dfe..8df6cff34 100644 --- a/src/core/client/ui/components/MatchMedia/MatchMedia.tsx +++ b/src/core/client/ui/components/MatchMedia/MatchMedia.tsx @@ -2,6 +2,8 @@ import React from "react"; import { ReactNode, StatelessComponent } from "react"; import Responsive, { MediaQueryMatchers } from "react-responsive"; +import { PropTypesOf } from "talk-ui/types"; + import theme from "../../theme/variables"; import UIContext from "../UIContext"; @@ -45,3 +47,4 @@ const MatchMediaWithContext: StatelessComponent = props => ( ); export default MatchMediaWithContext; +export type MatchMediaProps = PropTypesOf; diff --git a/src/core/client/ui/components/MatchMedia/index.ts b/src/core/client/ui/components/MatchMedia/index.ts index 71f16f6f8..66da09460 100644 --- a/src/core/client/ui/components/MatchMedia/index.ts +++ b/src/core/client/ui/components/MatchMedia/index.ts @@ -1,2 +1 @@ -export * from "./MatchMedia"; -export { default } from "./MatchMedia"; +export { default, MatchMediaProps } from "./MatchMedia"; diff --git a/src/core/client/ui/components/RelativeTime/RelativeTime.css b/src/core/client/ui/components/RelativeTime/RelativeTime.css index 3faa5e162..e0450679f 100644 --- a/src/core/client/ui/components/RelativeTime/RelativeTime.css +++ b/src/core/client/ui/components/RelativeTime/RelativeTime.css @@ -1,4 +1,4 @@ .root { - composes: body1 from "talk-ui/shared/typography.css"; + composes: bodyCopy from "talk-ui/shared/typography.css"; background-color: transparent; } diff --git a/src/core/client/ui/components/RelativeTime/RelativeTime.tsx b/src/core/client/ui/components/RelativeTime/RelativeTime.tsx index 3a8022656..265a64978 100644 --- a/src/core/client/ui/components/RelativeTime/RelativeTime.tsx +++ b/src/core/client/ui/components/RelativeTime/RelativeTime.tsx @@ -1,9 +1,9 @@ import cn from "classnames"; -import React from "react"; +import React, { Ref } from "react"; import TimeAgo, { Formatter } from "react-timeago"; import { UIContext } from "talk-ui/components"; -import { withStyles } from "talk-ui/hocs"; +import { withForwardRef, withStyles } from "talk-ui/hocs"; import { PropTypesOf } from "talk-ui/types"; import * as styles from "./RelativeTime.css"; @@ -14,29 +14,30 @@ interface InnerProps { classes: typeof styles; className?: string; formatter?: Formatter; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; } const defaultFormatter: Formatter = (value, unit, suffix, timestamp: string) => new Date(timestamp).toISOString(); -class RelativeTime extends React.Component { - public render() { - const { date, classes, live, className, formatter } = this.props; - return ( - - {({ timeagoFormatter }) => ( - - )} - - ); - } -} +const RelativeTime: React.StatelessComponent = props => { + const { date, classes, live, className, formatter } = props; + return ( + + {({ timeagoFormatter }) => ( + + )} + + ); +}; -const enhanced = withStyles(styles)(RelativeTime); +const enhanced = withForwardRef(withStyles(styles)(RelativeTime)); export type RelativeTimeProps = PropTypesOf; export default enhanced; diff --git a/src/core/client/ui/components/RelativeTime/index.ts b/src/core/client/ui/components/RelativeTime/index.ts index 8bc2dfb5e..90bd4c0ca 100644 --- a/src/core/client/ui/components/RelativeTime/index.ts +++ b/src/core/client/ui/components/RelativeTime/index.ts @@ -1,2 +1 @@ -export * from "./RelativeTime"; -export { default } from "./RelativeTime"; +export { default, RelativeTimeProps } from "./RelativeTime"; diff --git a/src/core/client/ui/components/ToggleShow/ToggleShow.spec.tsx b/src/core/client/ui/components/ToggleShow/ToggleShow.spec.tsx new file mode 100644 index 000000000..d81075019 --- /dev/null +++ b/src/core/client/ui/components/ToggleShow/ToggleShow.spec.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { create } from "react-test-renderer"; + +import ToggleShow from "./ToggleShow"; + +it("renders correctly", () => { + const tree = create( + + {({ toggleShow, show }) => ( +
+ {show &&
SHOW ME
} + +
+ )} +
+ ).toJSON(); + + expect(tree).toMatchSnapshot(); +}); + +it("should hide the div", () => { + const renderer = create( + + {({ toggleShow, show }) => ( +
+ {show &&
SHOW ME
} + +
+ )} +
+ ); + + renderer.root.findByType("button").props.onClick(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/ToggleShow/ToggleShow.tsx b/src/core/client/ui/components/ToggleShow/ToggleShow.tsx new file mode 100644 index 000000000..d8830c56e --- /dev/null +++ b/src/core/client/ui/components/ToggleShow/ToggleShow.tsx @@ -0,0 +1,33 @@ +import React, { ReactNode } from "react"; + +interface State { + show: boolean; +} + +interface Props { + children: (props: RenderProps) => ReactNode; +} + +interface RenderProps { + toggleShow: () => void; + show: boolean; +} + +class ToggleShow extends React.Component { + public state = { + show: true, + }; + + public toggleShow = () => { + this.setState(state => ({ show: !state.show })); + }; + + public render() { + return this.props.children({ + toggleShow: this.toggleShow, + show: this.state.show, + }); + } +} + +export default ToggleShow; diff --git a/src/core/client/ui/components/ToggleShow/ToogleShow.mdx b/src/core/client/ui/components/ToggleShow/ToogleShow.mdx new file mode 100644 index 000000000..4fc58d41e --- /dev/null +++ b/src/core/client/ui/components/ToggleShow/ToogleShow.mdx @@ -0,0 +1,22 @@ +--- +name: ToggleShow +menu: UI Kit +--- + +import { Playground } from 'docz' +import ToggleShow from './ToggleShow' + +# ToggleShow +A Component that provides a render function to display nodes + +## Basic usage + + + {({ toggleShow, show }) => ( +
+ {show &&
SHOW ME
} + +
+ )} +
+
diff --git a/src/core/client/ui/components/ToggleShow/__snapshots__/ToggleShow.spec.tsx.snap b/src/core/client/ui/components/ToggleShow/__snapshots__/ToggleShow.spec.tsx.snap new file mode 100644 index 000000000..323274282 --- /dev/null +++ b/src/core/client/ui/components/ToggleShow/__snapshots__/ToggleShow.spec.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+
+ SHOW ME +
+ +
+`; + +exports[`should hide the div 1`] = ` +
+ +
+`; diff --git a/src/core/client/ui/components/TrapFocus/TrapFocus.mdx b/src/core/client/ui/components/TrapFocus/TrapFocus.mdx new file mode 100644 index 000000000..d5c209aa9 --- /dev/null +++ b/src/core/client/ui/components/TrapFocus/TrapFocus.mdx @@ -0,0 +1,39 @@ +--- +name: TrapFocus +menu: UI Kit +--- + +import { Playground } from 'docz' +import TrapFocus from './TrapFocus' + +# TrapFocus + +Traps focus inside component when using keyboard navigation for accessibility purposes. + +## Basic usage + +```ts +import React from "react"; + +class Dialog extends React.Component { + private firstFocusable: HTMLElement; + private lastFocusable: HTMLElement; + + private setFirstFocusable = (ref: HTMLElement) => (this.firstFocusable = ref); + private setLastFocusable = (ref: HTMLElement) => (this.lastFocusable = ref); + + public render() { + return ( +
+ + + + +
+ ); + } +} +``` diff --git a/src/core/client/ui/components/TrapFocus/TrapFocus.spec.tsx b/src/core/client/ui/components/TrapFocus/TrapFocus.spec.tsx new file mode 100644 index 000000000..f4932775f --- /dev/null +++ b/src/core/client/ui/components/TrapFocus/TrapFocus.spec.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { create } from "react-test-renderer"; +import Sinon from "sinon"; + +import { PropTypesOf } from "talk-ui/types"; +import TrapFocus from "./TrapFocus"; + +it("renders correctly", () => { + const props: PropTypesOf = { + firstFocusable: null, + lastFocusable: null, + children: ( + <> + child1 + child2 + + ), + }; + const renderer = create( +
+ +
+ ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("Change focus to `lastFocusable` when focus reaches beginning", () => { + const fakeHTMLElementBegin = { focus: Sinon.spy() }; + const fakeHTMLElementEnd = { focus: Sinon.spy() }; + const props: PropTypesOf = { + firstFocusable: fakeHTMLElementBegin as any, + lastFocusable: fakeHTMLElementEnd as any, + children: ( + <> + child1 + child2 + + ), + }; + const renderer = create( +
+ +
+ ); + renderer.root.findAllByProps({ tabIndex: 0 })[0].props.onFocus(); + expect(fakeHTMLElementBegin.focus.called).toBe(false); + expect(fakeHTMLElementEnd.focus.called).toBe(true); +}); + +it("Change focus to `firstFocusable` when focus reaches the end", () => { + const fakeHTMLElementBegin = { focus: Sinon.spy() }; + const fakeHTMLElementEnd = { focus: Sinon.spy() }; + const props: PropTypesOf = { + firstFocusable: fakeHTMLElementBegin as any, + lastFocusable: fakeHTMLElementEnd as any, + children: ( + <> + child1 + child2 + + ), + }; + const renderer = create( +
+ +
+ ); + renderer.root.findAllByProps({ tabIndex: 0 })[1].props.onFocus(); + expect(fakeHTMLElementBegin.focus.called).toBe(true); + expect(fakeHTMLElementEnd.focus.called).toBe(false); +}); diff --git a/src/core/client/ui/components/TrapFocus/TrapFocus.tsx b/src/core/client/ui/components/TrapFocus/TrapFocus.tsx new file mode 100644 index 000000000..a50b50d6c --- /dev/null +++ b/src/core/client/ui/components/TrapFocus/TrapFocus.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +export interface Focusable { + focus: () => void; +} + +export interface TrapFocusProps { + firstFocusable: Focusable | null; + lastFocusable: Focusable | null; + children: React.ReactNode; +} + +export default class TrapFocus extends React.Component { + // Trap keyboard focus inside the dropdown until a value has been chosen. + public focusBegin = () => this.props.firstFocusable!.focus(); + public focusEnd = () => this.props.lastFocusable!.focus(); + + public render() { + return ( + <> +
+ {this.props.children} +
+ + ); + } +} diff --git a/src/core/client/ui/components/TrapFocus/__snapshots__/TrapFocus.spec.tsx.snap b/src/core/client/ui/components/TrapFocus/__snapshots__/TrapFocus.spec.tsx.snap new file mode 100644 index 000000000..b39ad808e --- /dev/null +++ b/src/core/client/ui/components/TrapFocus/__snapshots__/TrapFocus.spec.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+
+ + child1 + + + child2 + +
+
+`; diff --git a/src/core/client/ui/components/TrapFocus/index.ts b/src/core/client/ui/components/TrapFocus/index.ts new file mode 100644 index 000000000..2bb55f23d --- /dev/null +++ b/src/core/client/ui/components/TrapFocus/index.ts @@ -0,0 +1 @@ +export { default, TrapFocusProps } from "./TrapFocus"; diff --git a/src/core/client/ui/components/Typography/Typography.css b/src/core/client/ui/components/Typography/Typography.css index a3c19a400..8b0edc3b8 100644 --- a/src/core/client/ui/components/Typography/Typography.css +++ b/src/core/client/ui/components/Typography/Typography.css @@ -19,28 +19,16 @@ composes: heading4 from "talk-ui/shared/typography.css"; } -.subtitle1 { - composes: subtitle1 from "talk-ui/shared/typography.css"; -} - -.subtitle2 { - composes: subtitle2 from "talk-ui/shared/typography.css"; -} - -.body1 { - composes: body1 from "talk-ui/shared/typography.css"; -} - -.body2 { - composes: body2 from "talk-ui/shared/typography.css"; +.bodyCopy { + composes: bodyCopy from "talk-ui/shared/typography.css"; } .button { composes: button from "talk-ui/shared/typography.css"; } -.overline { - composes: overline from "talk-ui/shared/typography.css"; +.buttonLarge { + composes: buttonLarge from "talk-ui/shared/typography.css"; } .timestamp { @@ -85,8 +73,8 @@ color: var(--palette-primary-main); } -.colorSecondary { - color: var(--palette-secondary-main); +.colorTextPrimary { + color: var(--palette-text-primary); } .colorTextSecondary { @@ -96,3 +84,7 @@ .colorError { color: var(--palette-error-main); } + +.colorSuccess { + color: var(--palette-success-main); +} diff --git a/src/core/client/ui/components/Typography/Typography.mdx b/src/core/client/ui/components/Typography/Typography.mdx new file mode 100644 index 000000000..8833e95f9 --- /dev/null +++ b/src/core/client/ui/components/Typography/Typography.mdx @@ -0,0 +1,56 @@ +--- +name: Typography +menu: UI Kit +--- + +import { Playground } from 'docz' +import Typography from './Typography' +import Flex from '../Flex' + +# Typography + +## Basic Use + + + Heading1 + Heading2 + Heading3 + Heading4 + BodyCopy + timestamp + + + +## Using different colors + + + textPrimary + textSecondary + success + error + + + +## Set align + + + left + center + right + No hay nadie que ame el dolor mismo, que lo busque, lo encuentre y lo + quiera, simplemente porque es el dolor. + + + +## Cut off long text + + No hay nadie que ame el dolor mismo, que lo busque, lo encuentre y lo quiera, simplemente porque es el dolor. + + +## Gutters + + I have a bottom gutter + I have a paragraph gutter + Just a normal text + + diff --git a/src/core/client/ui/components/Typography/Typography.spec.tsx b/src/core/client/ui/components/Typography/Typography.spec.tsx new file mode 100644 index 000000000..d694869f8 --- /dev/null +++ b/src/core/client/ui/components/Typography/Typography.spec.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; + +import { PropTypesOf } from "talk-ui/types"; + +import Typography from "./Typography"; + +it("renders correctly", () => { + const props: PropTypesOf = { + className: "custom", + variant: "heading1", + color: "textSecondary", + gutterBottom: true, + children: "Hello World", + noWrap: true, + align: "left", + }; + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/Typography/Typography.tsx b/src/core/client/ui/components/Typography/Typography.tsx index b743305e6..752f1f828 100644 --- a/src/core/client/ui/components/Typography/Typography.tsx +++ b/src/core/client/ui/components/Typography/Typography.tsx @@ -1,8 +1,8 @@ import cn from "classnames"; -import React from "react"; +import React, { Ref } from "react"; import { HTMLAttributes, ReactNode, StatelessComponent } from "react"; -import { withStyles } from "talk-ui/hocs"; +import { withForwardRef, withStyles } from "talk-ui/hocs"; import { PropTypesOf } from "talk-ui/types"; import * as styles from "./Typography.css"; @@ -12,11 +12,7 @@ type Variant = | "heading2" | "heading3" | "heading4" - | "subtitle1" - | "subtitle2" - | "body1" - | "body2" - | "button" + | "bodyCopy" | "timestamp"; // Based on Typography Component of Material UI. @@ -45,10 +41,10 @@ interface InnerProps extends HTMLAttributes { color?: | "inherit" | "primary" + | "textPrimary" | "textSecondary" - | "secondary" | "error" - | "default"; + | "success"; /** * The component used for the root node. * Either a string to use a DOM element or a component. @@ -77,6 +73,9 @@ interface InnerProps extends HTMLAttributes { * Applies the theme typography styles. */ variant?: Variant; + + /** Internal: Forwarded Ref */ + forwardRef?: Ref; } const Typography: StatelessComponent = props => { @@ -91,6 +90,7 @@ const Typography: StatelessComponent = props => { noWrap, paragraph, variant, + forwardRef, ...rest } = props; @@ -98,10 +98,11 @@ const Typography: StatelessComponent = props => { classes.root, classes[variant!], { - [classes.colorPrimary]: color === "primary", - [classes.colorSecondary]: color === "secondary", - [classes.colorError]: color === "error", + [classes.colorTextPrimary]: color === "textPrimary", [classes.colorTextSecondary]: color === "textSecondary", + [classes.colorPrimary]: color === "primary", + [classes.colorError]: color === "error", + [classes.colorSuccess]: color === "success", [classes.noWrap]: noWrap, [classes.gutterBottom]: gutterBottom, [classes.paragraph]: paragraph, @@ -116,29 +117,26 @@ const Typography: StatelessComponent = props => { const Component = component || (paragraph ? "p" : headlineMapping![variant!]) || "span"; - return ; + return ; }; Typography.defaultProps = { align: "inherit", - color: "default", + color: "textPrimary", gutterBottom: false, headlineMapping: { heading1: "h1", heading2: "h1", heading3: "h1", heading4: "h1", - subtitle1: "h2", - subtitle2: "h3", - body1: "p", - body2: "aside", + bodyCopy: "p", timestamp: "span", }, noWrap: false, paragraph: false, - variant: "body1", + variant: "bodyCopy", }; -const enhanced = withStyles(styles)(Typography); -export type CenterProps = PropTypesOf; +const enhanced = withForwardRef(withStyles(styles)(Typography)); +export type TypographyProps = PropTypesOf; export default enhanced; diff --git a/src/core/client/ui/components/Typography/__snapshots__/Typography.spec.tsx.snap b/src/core/client/ui/components/Typography/__snapshots__/Typography.spec.tsx.snap new file mode 100644 index 000000000..66abdab95 --- /dev/null +++ b/src/core/client/ui/components/Typography/__snapshots__/Typography.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +

+ Hello World +

+`; diff --git a/src/core/client/ui/components/Typography/index.ts b/src/core/client/ui/components/Typography/index.ts index 1557f9552..45ed99c30 100644 --- a/src/core/client/ui/components/Typography/index.ts +++ b/src/core/client/ui/components/Typography/index.ts @@ -1,2 +1 @@ -export * from "./Typography"; -export { default } from "./Typography"; +export { default, TypographyProps } from "./Typography"; diff --git a/src/core/client/ui/components/index.ts b/src/core/client/ui/components/index.ts index 3d81f1aba..91ddaeb68 100644 --- a/src/core/client/ui/components/index.ts +++ b/src/core/client/ui/components/index.ts @@ -5,3 +5,4 @@ export { default as RelativeTime } from "./RelativeTime"; export { default as UIContext, UIContextProps } from "./UIContext"; export { default as Flex } from "./Flex"; export { default as MatchMedia } from "./MatchMedia"; +export { default as TrapFocus } from "./TrapFocus"; diff --git a/src/core/client/ui/hocs/index.ts b/src/core/client/ui/hocs/index.ts index 1c49cdc24..6102cc5ce 100644 --- a/src/core/client/ui/hocs/index.ts +++ b/src/core/client/ui/hocs/index.ts @@ -1,2 +1,4 @@ export { default as withStyles } from "./withStyles"; export { default as withKeyboardFocus } from "./withKeyboardFocus"; +export { default as withMouseHover } from "./withMouseHover"; +export { default as withForwardRef } from "./withForwardRef"; diff --git a/src/core/client/ui/hocs/withForwardRef.tsx b/src/core/client/ui/hocs/withForwardRef.tsx new file mode 100644 index 000000000..883923634 --- /dev/null +++ b/src/core/client/ui/hocs/withForwardRef.tsx @@ -0,0 +1,36 @@ +import React, { Ref } from "react"; + +// TODO: ForwardRef API is not supported in enzymes shallow rendering +// https://github.com/airbnb/enzyme/issues/1553 +// As for now we do props passing instead until full React16 support lands in +// enzyme. + +/** + * withForwardRef provides a property called `forwardRef` using + * the `React.forwardRef` api. + */ +/* +function withForwardRef

; forwardRef?: Ref }>( + BaseComponent: React.ComponentType

+): React.ComponentType> { + const forwardRef: RefForwardingComponent = (props, ref) => ( + + ); + return React.forwardRef(forwardRef); +} + +// TODO: workaround, add bug link. +export default withForwardRef as < + P extends { ref?: Ref; forwardRef?: Ref } +>( + BaseComponent: React.ComponentType

+) => React.ComponentType

; + +*/ + +// Stub, currently doesn't do anything except adding types. +export default function withForwardRef

}>( + BaseComponent: React.ComponentType

+): React.ComponentType

{ + return BaseComponent; +} diff --git a/src/core/client/ui/hocs/withKeyboardFocus.tsx b/src/core/client/ui/hocs/withKeyboardFocus.tsx index eaae1529d..dff7d0e1e 100644 --- a/src/core/client/ui/hocs/withKeyboardFocus.tsx +++ b/src/core/client/ui/hocs/withKeyboardFocus.tsx @@ -3,10 +3,10 @@ import { FocusEvent, MouseEvent } from "react"; import { hoistStatics } from "recompose"; interface InjectedProps { - onFocus?: React.EventHandler>; - onBlur?: React.EventHandler>; - onMouseDown?: React.EventHandler>; - keyboardFocus?: boolean; + onFocus: React.EventHandler>; + onBlur: React.EventHandler>; + onMouseDown: React.EventHandler>; + keyboardFocus: boolean; } /** @@ -14,8 +14,8 @@ interface InjectedProps { * to indicate a focus on the element, that wasn't triggered by mouse * or touch. */ -export default hoistStatics( - (WrappedComponent: React.ComponentType) => { +const withKeyboardFocus = hoistStatics( + (BaseComponent: React.ComponentType) => { class WithKeyboardFocus extends React.Component { public state = { keyboardFocus: false, @@ -48,7 +48,7 @@ export default hoistStatics( public render() { return ( - ( return WithKeyboardFocus as React.ComponentType; } ); + +// TODO: workaround, add bug link. +export default withKeyboardFocus as

>( + BaseComponent: React.ComponentType

+) => React.ComponentType

; diff --git a/src/core/client/ui/hocs/withMouseHover.tsx b/src/core/client/ui/hocs/withMouseHover.tsx new file mode 100644 index 000000000..8172012f0 --- /dev/null +++ b/src/core/client/ui/hocs/withMouseHover.tsx @@ -0,0 +1,69 @@ +import * as React from "react"; +import { MouseEvent, TouchEvent } from "react"; +import { hoistStatics } from "recompose"; + +interface InjectedProps { + onMouseOver: React.EventHandler>; + onMouseOut: React.EventHandler>; + onTouchEnd: React.EventHandler>; + mouseHover: boolean; +} + +/** + * withMouseHover provides a property `MouseHover: boolean` + * to indicate a focus on the element, that wasn't triggered by mouse + * or touch. + */ +const withMouseHover = hoistStatics( + (BaseComponent: React.ComponentType) => { + class WithMouseHover extends React.Component { + public state = { + mouseHover: false, + lastTouchEndTime: 0, + }; + + private handleTouchEnd: React.EventHandler> = event => { + if (this.props.onTouchEnd) { + this.props.onTouchEnd(event); + } + this.setState({ lastTouchEndTime: new Date().getTime() }); + }; + + private handleMouseOver: React.EventHandler> = event => { + if (this.props.onMouseOver) { + this.props.onMouseOver(event); + } + const now = new Date().getTime(); + if (now - this.state.lastTouchEndTime > 750) { + this.setState({ mouseHover: true }); + } + }; + + private handleMouseOut: React.EventHandler> = event => { + if (this.props.onMouseOut) { + this.props.onMouseOut(event); + } + this.setState({ mouseHover: false }); + }; + + public render() { + return ( + + ); + } + } + + return WithMouseHover as React.ComponentType; + } +); + +// TODO: workaround, add bug link. +export default withMouseHover as

>( + BaseComponent: React.ComponentType

+) => React.ComponentType

; diff --git a/src/core/client/ui/hocs/withStyles.ts b/src/core/client/ui/hocs/withStyles.ts index 0ae53e860..d8ae4313b 100644 --- a/src/core/client/ui/hocs/withStyles.ts +++ b/src/core/client/ui/hocs/withStyles.ts @@ -13,17 +13,18 @@ function withStyles( ): DefaultingInferableComponentEnhancer<{ classes?: Partial }> { const classes = { ...(styles as any) }; return withPropsOnChange(["classes"], props => { + const resolvedClasses = { ...classes }; if (props.classes) { Object.keys(props.classes).forEach(k => { if (classes[k]) { - classes[k] += ` ${props.classes[k]}`; + resolvedClasses[k] += ` ${props.classes[k]}`; } else if (process.env.NODE_ENV !== "production") { // tslint:disable:next-line: no-console console.warn("Extending non existant className", k); } }); } - return { classes }; + return { classes: resolvedClasses }; }); } diff --git a/src/core/client/ui/shared/typography.css b/src/core/client/ui/shared/typography.css index 3cb483cd2..9e52cfc89 100644 --- a/src/core/client/ui/shared/typography.css +++ b/src/core/client/ui/shared/typography.css @@ -1,3 +1,61 @@ +@font-face { + font-family: "Source Sans Pro"; + font-style: normal; + font-display: swap; + font-weight: 300; + src: local("Source Sans Pro Light "), local("Source Sans Pro-Light"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-300.woff2") + format("woff2"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-300.woff") + format("woff"); +} + +@font-face { + font-family: "Source Sans Pro"; + font-style: normal; + font-display: swap; + font-weight: 400; + src: local("Source Sans Pro Regular italic"), + local("Source Sans Pro-Regularitalic"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-400.woff2") + format("woff2"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-400.woff") + format("woff"); +} + +@font-face { + font-family: "Source Sans Pro"; + font-style: normal; + font-display: swap; + font-weight: 600; + src: local("Source Sans Pro SemiBold italic"), + local("Source Sans Pro-SemiBolditalic"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-600.woff2") + format("woff2"), + url("typeface-source-sans-pro/files/source-sans-pro-latin-600.woff") + format("woff"); +} + +@font-face { + font-family: "Manuale"; + font-style: normal; + font-display: swap; + font-weight: 400; + src: local("Manuale Regular "), local("Manuale-Regular"), + url("typeface-manuale/files/manuale-latin-400.woff2") format("woff2"), + url("typeface-manuale/files/manuale-latin-400.woff") format("woff"); +} + +@font-face { + font-family: "Manuale"; + font-style: normal; + font-display: swap; + font-weight: 600; + src: local("Manuale SemiBold "), local("Manuale-SemiBold"), + url("typeface-manuale/files/manuale-latin-600.woff2") format("woff2"), + url("typeface-manuale/files/manuale-latin-600.woff") format("woff"); +} + .heading1 { font-size: calc(24rem / var(--rem-base)); font-weight: var(--font-weight-medium); @@ -33,16 +91,7 @@ color: var(--palette-text-primary); } -.subtitle1 { -} - -.subtitle2 { -} - -.body2 { -} - -.body1 { +.bodyCopy { font-size: calc(16rem / var(--rem-base)); font-weight: var(--font-weight-regular); font-family: "Source Sans Pro"; @@ -55,11 +104,18 @@ color: var(--palette-text-secondary); font-family: "Source Sans Pro"; font-weight: var(--font-weight-medium); - font-size: 16px; + font-size: 14px; + line-height: calc(18em / 16); letter-spacing: calc(0.57em / 16); } -.overline { +.buttonLarge { + color: var(--palette-text-secondary); + font-family: "Source Sans Pro"; + font-weight: var(--font-weight-medium); + font-size: 16px; + line-height: calc(20em / 16); + letter-spacing: calc(0.57em / 16); } .timestamp { diff --git a/src/core/client/ui/theme/variables.ts b/src/core/client/ui/theme/variables.ts index d68b88a41..415a5c32f 100644 --- a/src/core/client/ui/theme/variables.ts +++ b/src/core/client/ui/theme/variables.ts @@ -12,14 +12,16 @@ const variables = { main: "#3498DB", light: "#67B2E4", lighter: "#8DC5EB", + lightest: "#EBF5FB", }, /* Secondary colors */ - secondary: { + grey: { darkest: "#404345", dark: "#65696B", main: "#787D80", light: "#9A9DA0", - lighter: "#636E72", + lighter: "#BBBEBF", + lightest: "#F5F5F5", }, /* Success colors */ success: { @@ -28,21 +30,21 @@ const variables = { main: "#00CD73", light: "#40D996", lighter: "#83EBBD", + lightest: "#E6FAF1", }, /* Error colors */ error: { darkest: "#F50F0C", dark: "#FF1F1C", main: "#FA4643", - light: "#F26563", - lighter: "#F26563", + light: "#FB7472", + lighter: "#FC9795", + lightest: "#FEF0EF", }, /* Text colors */ text: { primary: "#3B4A53", secondary: "#787D80", - disabled: "rgba(0, 0, 0, 0.38)", - hint: "rgba(0, 0, 0, 0.38)", }, /* Common colors */ common: { @@ -51,11 +53,6 @@ const variables = { }, /* Divider */ divider: "rgba(0, 0, 0, 0.12)", - /* The background colors used to style the surfaces. */ - background: { - paper: "#FFF", - default: "#FAFAFA", - }, }, /* gitter and spacing */ spacingUnitSmall: 5, @@ -68,7 +65,7 @@ const variables = { fontSize: 16, fontWeightLight: 300, fontWeightRegular: 400, - fontWeightMedium: 550, + fontWeightMedium: 600, /* Breakpoints */ breakpoints: { xs: 320, diff --git a/src/core/client/ui/types.ts b/src/core/client/ui/types.ts index 576b881c3..55c7d638b 100644 --- a/src/core/client/ui/types.ts +++ b/src/core/client/ui/types.ts @@ -2,27 +2,19 @@ import React from "react"; // TODO: Extract useful common types into its own package. -/** - * Returns literals types that are in T but not in U. - * - * E.g. Diff<"a" | "b", "a"> = "b" - */ -export type Diff = ({ [P in T]: P } & - { [P in U]: never } & { [x: string]: never; [x: number]: never })[T]; - /** * Overwrite properties of `T`. * * E.g. Omit<{a: boolean, b: boolean}, "b"> = {a: boolean} */ -export type Omit = Pick>; +export type Omit = Pick>; /** * Overwrite properties of `T`. * * E.g. Overwrite<{a: boolean}, {a: string}> = {a: string} */ -export type Overwrite = Pick> & U; +export type Overwrite = Pick> & U; /** * Returns the PropTypes from a React Component. diff --git a/src/locales/de/framework.ftl b/src/locales/de/framework.ftl index 2f5a6d472..55dca1c8c 100644 --- a/src/locales/de/framework.ftl +++ b/src/locales/de/framework.ftl @@ -22,7 +22,7 @@ framework-timeago = *[other] Minuten } [hour] { $value -> - [0] Stunde + [1] Stunde *[other] Stunden } [day] { $value -> diff --git a/src/locales/en-US/framework.ftl b/src/locales/en-US/framework.ftl index 5d88f9a7e..555637497 100644 --- a/src/locales/en-US/framework.ftl +++ b/src/locales/en-US/framework.ftl @@ -18,7 +18,7 @@ framework-timeago-time = *[other] minutes } [hour] { $value -> - [0] hour + [1] hour *[other] hours } [day] { $value -> diff --git a/src/locales/es/framework.ftl b/src/locales/es/framework.ftl index 2b95d944c..a383c75bb 100644 --- a/src/locales/es/framework.ftl +++ b/src/locales/es/framework.ftl @@ -22,7 +22,7 @@ framework-timeago = *[other] minutos } [hour] { $value -> - [0] hora + [1] hora *[other] horas } [day] { $value ->