diff --git a/www/jest-preprocess.js b/www/jest-preprocess.js index 6240240d7d9e9..2d59111bc3506 100644 --- a/www/jest-preprocess.js +++ b/www/jest-preprocess.js @@ -1,5 +1,6 @@ const babelOptions = { presets: [`babel-preset-gatsby`], + plugins: [`emotion`], } module.exports = require(`babel-jest`).createTransformer(babelOptions) diff --git a/www/package.json b/www/package.json index a56c4c4045dc9..a0c95811cfee0 100644 --- a/www/package.json +++ b/www/package.json @@ -8,6 +8,7 @@ "@emotion/styled": "^10.0.7", "@reach/skip-nav": "^0.1.1", "@reach/visually-hidden": "^0.1.2", + "@xstate/react": "^0.2.1", "axios": "^0.18.0", "bluebird": "^3.5.1", "dotenv": "^6.0.0", @@ -77,6 +78,7 @@ "slugify": "^1.3.0", "typography": "^1.0.0-alpha.4", "typography-plugin-code": "^1.0.0-alpha.0", + "xstate": "^4.5.0", "zipkin": "^0.14.2", "zipkin-javascript-opentracing": "^2.0.0", "zipkin-transport-http": "^0.14.2" diff --git a/www/src/components/docsearch-content.js b/www/src/components/docsearch-content.js index 95a7033f146b2..bbb3c1e461a3c 100644 --- a/www/src/components/docsearch-content.js +++ b/www/src/components/docsearch-content.js @@ -1,7 +1,9 @@ import React from "react" +import FeedbackWidget from "./feedback-widget/lazy-feedback-widget" export default ({ children }) => (
{children} +
) diff --git a/www/src/components/feedback-widget/buttons.js b/www/src/components/feedback-widget/buttons.js new file mode 100644 index 0000000000000..a49967f28f1f4 --- /dev/null +++ b/www/src/components/feedback-widget/buttons.js @@ -0,0 +1,234 @@ +import React, { Fragment } from "react" +import { css } from "@emotion/core" +import styled from "@emotion/styled" +import { keyframes } from "@emotion/core" +import { ScreenReaderText } from "./styled-elements" +import MdQuestionMark from "./question-mark-icon" +import { + mediaQueries, + colors, + fontSizes, + radii, + shadows, + space, +} from "../../utils/presets" + +const rotation = keyframes` + 0% { + transform: translateX(${space[1]}) rotate(0deg); + } + 100% { + transform: translateX(${space[1]}) rotate(360deg); + } +` + +export const focusStyle = css` + box-shadow: 0 0 0 0.12rem ${colors.accent}; + outline: none; +` + +const buttonStyles = css` + -webkit-appearance: none; + align-items: center; + background: ${colors.gatsby}; + border: none; + border-radius: ${radii[2]}px; + color: ${colors.white}; + cursor: pointer; + display: flex; + font-size: ${fontSizes[1]}; + padding: ${space[2]} ${space[3]}; + transition: 0.5s; + z-index: 1; + + svg { + height: ${space[4]}; + transform: translateX(${space[1]}); + width: ${space[4]}; + } + + &:focus { + ${focusStyle} + } + + &:disabled { + cursor: not-allowed; + opacity: 0.9; + } + + &:hover { + box-shadow: 0 0 0 0.12rem ${colors.accent}88; + } +` + +export const SubmitButton = styled(`button`)` + ${buttonStyles}; + + .submitting & { + svg { + animation: ${rotation} 1s linear infinite; + } + } + + @media screen and (prefers-reduced-motion: reduce) { + .submitting & { + svg { + animation: none; + } + } + } +` + +export const CloseButton = styled(`button`)` + ${buttonStyles}; + background: ${colors.white}; + border: 1px solid ${colors.gray.border}; + color: ${colors.gatsby}; +` + +export const ToggleButtonLabel = styled(`span`)` + align-items: center; + background: ${colors.white}; + border: 1px solid ${colors.gray.border}; + border-radius: ${radii[2]}px; + display: flex; + height: 2.5rem; + padding: 0 ${space[8]} 0 ${space[3]}; + transition: 0.5s; + white-space: nowrap; + width: 100%; + + ${mediaQueries.lg} { + box-shadow: ${shadows.floating}; + width: auto; + } +` + +export const ToggleButtonIcon = styled(`span`)` + align-items: center; + background: ${colors.lilac}; + border-radius: ${radii[6]}; + color: ${colors.white}; + display: flex; + font-size: ${fontSizes[1]}; + height: ${space[6]}; + justify-content: center; + position: absolute; + right: ${space[3]}; + transform: scale(1); + transition: 0.5s; + width: ${space[6]}; + + svg { + fill: ${colors.white}; + height: ${space[3]}; + width: ${space[3]}; + transition: 0.5s; + } + + ${mediaQueries.lg} { + .opened &, + .failed &, + .success &, + .submitting & { + &:hover { + svg { + transform: rotate(90deg); + fill: ${colors.accent}; + } + } + } + } + + @media screen and (prefers-reduced-motion: reduce) { + transition: 0; + } +` + +export const ToggleButton = styled(`button`)` + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + position: relative; + transition: 0.6s ease; + width: 100%; + z-index: 3; + + &:hover { + ${ToggleButtonLabel} { + box-shadow: 0 0 0 0.12rem ${colors.accent}88; + } + } + + &:focus { + outline: none; + + ${ToggleButtonLabel} { + ${focusStyle} + } + } + + ${props => + props.supressFocusAnimation && + css` + ${ToggleButtonLabel} { + transition: 0s; + } + `} + + .opened &, + .failed &, + .success &, + .submitting & { + display: none; + } + + ${mediaQueries.lg} { + bottom: 0; + position: absolute; + right: 0; + width: auto; + + .opened &, + .failed &, + .success &, + .submitting & { + display: flex; + transform: translate(-${space[2]}, -26rem); + + ${ToggleButtonIcon} { + background: ${colors.white}; + border: 1px solid ${colors.gray.border}; + transform: scale(1.8); + + svg { + fill: ${colors.gatsby}; + } + } + + &:focus { + ${ToggleButtonIcon} { + ${focusStyle}; + } + } + } + } + + @media screen and (prefers-reduced-motion: reduce) { + transition: 0; + } +` + +export const OpenFeedbackWidgetButtonContent = ({ loading }) => ( + + + {loading ? `Loading ...` : `Was this doc helpful to you`} + ? + + + + + +) diff --git a/www/src/components/feedback-widget/feedback-form.js b/www/src/components/feedback-widget/feedback-form.js new file mode 100644 index 0000000000000..7e7e1474fb595 --- /dev/null +++ b/www/src/components/feedback-widget/feedback-form.js @@ -0,0 +1,164 @@ +import React, { Fragment } from "react" +import styled from "@emotion/styled" +import WidgetWrapper from "./widget-wrapper" +import { SubmitButton, CloseButton, focusStyle } from "./buttons" +import { Actions, Title, ScreenReaderText } from "./styled-elements" +import RatingOption from "./rating-option" +import MdSentimentDissatisfied from "react-icons/lib/md/sentiment-dissatisfied" +import MdSentimentNeutral from "react-icons/lib/md/sentiment-neutral" +import MdSentimentVerySatisfied from "react-icons/lib/md/sentiment-very-satisfied" +import MdSend from "react-icons/lib/md/send" +import MdRefresh from "react-icons/lib/md/refresh" + +import { colors, fontSizes, radii, space } from "../../utils/presets" + +const Form = styled(`form`)` + margin-bottom: 0; +` + +const Fieldset = styled(`fieldset`)` + border: 0; + margin: 0 0 ${space[4]}; + padding: 0; +` + +const Legend = styled(`legend`)` + display: inline-block; + font-size: ${fontSizes[1]}; + margin-bottom: ${space[4]}; + padding: 0 ${space[2]}; + text-align: center; +` + +const Rating = styled(`div`)` + align-content: stretch; + border: 1px solid ${colors.lilac}; + border-radius: ${radii[3]}px; + display: flex; + flex: 1 1 auto; + justify-content: stretch; + overflow: hidden; + transition: 0.5s; + width: 99.99%; + + &:focus-within { + ${focusStyle} + } + + [disabled] & { + opacity: 0.5; + } +` + +const TextareaLabel = styled(`label`)` + font-size: ${fontSizes[1]}; + font-weight: bold; + + span { + font-weight: normal; + } +` + +const Textarea = styled(`textarea`)` + border: 1px solid ${colors.gray.light}; + border-radius: ${radii[2]}px; + display: block; + font-weight: normal; + height: 5.5rem; + margin: ${space[1]} 0 ${space[4]}; + padding: ${space[1]} ${space[2]}; + transition: 0.5s; + width: 100%; + + &:focus { + ${focusStyle} + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } +` + +const FeedbackForm = ({ + handleSubmit, + handleClose, + handleChange, + handleCommentChange, + titleRef, + rating, + comment, + submitting, +}) => ( + +
+ + Was this doc helpful to you? + +
+ Rate your experience + + + + + +
+ + Your comments (optional): +