Skip to content

Commit

Permalink
feat: Add match feedback inline (#264)
Browse files Browse the repository at this point in the history
* First pass at inline feedback widget

* Remove feedback URL where it's not needed

* Tweak messaging; border-box styles

* Update src/css/Feedback.scss

Co-authored-by: Rhys Mills <34686302+rhystmills@users.noreply.github.com>

* Align textarea with form to prevent a shrinking popover resulting in the cursor leaving the popover boundary

---------

Co-authored-by: Rhys Mills <34686302+rhystmills@users.noreply.github.com>
  • Loading branch information
jonathonherbert and rhystmills committed Apr 29, 2024
1 parent 4dd50cb commit f72421f
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 50 deletions.
1 change: 0 additions & 1 deletion pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ if (editorElement && sidebarNode) {
store,
commands,
overlayNode,
feedbackHref: "http://a-form-for-example.com",
onMarkCorrect: match => console.info("Match ignored!", match),
telemetryAdapter: typerighterTelemetryAdapter,
});
Expand Down
66 changes: 66 additions & 0 deletions src/css/Feedback.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
.Feedback__container {
margin-top: $gutter-width;
width: 100%;
font-size: $font-size-small;
a {
color: $match-gray;
}
}

.Feedback__responses {
width: 100%;
display: flex;
flex-direction: column;
gap: calc($gutter-width);
margin-top: calc($gutter-width / 2);
}

.Feedback__response {
display: flex;
align-items: center;
width: 100%;
border: none;
outline: none;
padding: $gutter-width;
gap: calc($gutter-width / 2);
border-radius: $base-border-radius;
font-weight: normal;
font-size: $font-size-base;
background-color: $match-feedback-background-color;
cursor: pointer;
&:hover {
background-color: darken($match-feedback-background-color, 5%);
}
svg {
color: darken($match-feedback-background-color, 40%);
}
}

.Feedback__feedback-detail {
width: 100%;
margin-top: calc($gutter-width / 2);
padding: calc($gutter-width / 2);
border-color: $match-gray;
border-radius: $base-border-radius;
font-family: inherit;
resize: vertical;
min-height: 110px;
}

.Feedback__response-send {
margin-top: calc($gutter-width / 2);
font-weight: bold;
background-color: $match-feedback-send-background-color;
color: white;
svg {
color: white;
}
&:hover {
background-color: $composer-primary-hover;
}
}

.Feedback__feedback-sent {
font-size: $font-size-base;
min-height: 100px;
}
10 changes: 3 additions & 7 deletions src/css/MatchWidget.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
transform: translate3d(0, -3px, 0);
z-index: 100;
overflow: hidden;
* {
box-sizing: border-box;
}
}

.MatchWidget__container {
Expand Down Expand Up @@ -145,10 +148,3 @@
.SidebarMatch__suggestion-list + .MatchWidget__ignore-match {
margin-top: 3px;
}

.MatchWidget__feedbackLink {
font-size: $font-size-small;
a {
color: $match-gray;
}
}
19 changes: 19 additions & 0 deletions src/css/index.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// Variables

$composer-primary: #00ADEE;
$composer-primary-hover: #008fc5;
$gutter-width: 8px;
$sidebar-border-color: #ddd;
$text-color-secondary: #5f5e5e;
Expand All @@ -12,6 +15,9 @@ $match-suggestion-background-color: #228816;
$match-suggestion-background-color-hover: #1e7014;
$match-suggestion-color: #fff;
$match-suggestion-color-hover: #fff;
$match-feedback-background-color: #e9e9e9;
$match-feedback-send-background-color: $composer-primary;
$match-feedback-send-background-color-hover: $composer-primary-hover;
$icon-fill-color-default: #999999;
$font-size-base: 15px;
$font-size-large: 1.1em;
Expand Down Expand Up @@ -43,6 +49,18 @@ $sidebar-highlight-color:#01adee;
}
}

// Animations;

.animation__pop-in {
animation-duration: 0.2s;
animation-name: animation__pop-in;
}

@keyframes animation__pop-in {
0% { opacity: 0; transform: translate3d(0, -10px, 0); }
100% { opacity: 1; transform: translate3d(0, 0, 0); }
}

// Imports
@import "./Controls.scss";
@import "./Sidebar.scss";
Expand All @@ -51,6 +69,7 @@ $sidebar-highlight-color:#01adee;
@import "./SuggestionList.scss";
@import "./MatchWidget.scss";
@import "./MatchDebug.scss";
@import "./Feedback.scss";

// Elements

Expand Down
119 changes: 119 additions & 0 deletions src/ts/components/Feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { useCallback, useContext, useState } from "react";
import { IMatch } from "../interfaces/IMatch";
import TelemetryContext from "../contexts/TelemetryContext";
import { MoreHoriz, Announcement, MailOutline } from "@mui/icons-material";

const responses = [
"This doesn't match the style guide.",
"This has been flagged incorrectly.",
"This is a valid word."
];

const FeedbackFormStates = {
CLOSED: "CLOSED",
SELECT_RESPONSE: "SELECT_RESPONSE",
ADD_DETAIL: "ADD_DETAIL",
FEEDBACK_SENT: "FEEDBACK_SENT"
} as const;

export const Feedback = ({
match,
documentUrl
}: {
match: IMatch;
documentUrl: string;
}) => {
const [formState, setFormState] = useState<keyof typeof FeedbackFormStates>(
FeedbackFormStates.CLOSED
);
const [inputValue, setInputValue] = useState<string>("");
const { telemetryAdapter } = useContext(TelemetryContext);
const sendFeedback = useCallback(
(e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
telemetryAdapter?.feedbackReceived(match, inputValue, documentUrl);
setFormState(FeedbackFormStates.FEEDBACK_SENT);
},
[setFormState, match, inputValue, documentUrl]
);

const getFormPanelFromState = useCallback(
(state: keyof typeof FeedbackFormStates) => {
switch (state) {
case FeedbackFormStates.CLOSED: {
return (
<a
href="#"
onClick={e => {
e.preventDefault();
setFormState(FeedbackFormStates.SELECT_RESPONSE);
}}
>
Issue with this result? Tell us!
</a>
);
}
case FeedbackFormStates.SELECT_RESPONSE: {
return (
<div className="Feedback__responses animation__pop-in">
{responses.map(response => (
<button
className="Feedback__response"
key={response}
onClick={() => {
setInputValue(response);
setFormState(FeedbackFormStates.ADD_DETAIL);
}}
>
<Announcement fontSize="small" /> {response}
</button>
))}
<button
className="Feedback__response"
onClick={() => {
setFormState(FeedbackFormStates.ADD_DETAIL);
}}
>
<MoreHoriz fontSize="small" /> Other
</button>
</div>
);
}
case FeedbackFormStates.ADD_DETAIL: {
return (
<div className="Feedback__add-detail animation__pop-in">
<div>
<label htmlFor="Feedback__feedback-detail">
Add additional information here
</label>
<textarea
id="Feedback__feedback-detail"
className="Feedback__feedback-detail"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
/>
</div>
<div
className="Feedback__response Feedback__response-send"
onClick={sendFeedback}
>
<MailOutline fontSize="small" />
Send feedback
</div>
</div>
);
}
case FeedbackFormStates.FEEDBACK_SENT: {
return <div className="Feedback__feedback-sent">Thanks for your feedback – we really appreciate it.</div>
}
}
},
[inputValue, setInputValue, formState, sendFeedback]
);

return (
<div className="Feedback__container">
{getFormPanelFromState(formState)}
</div>
);
};
44 changes: 9 additions & 35 deletions src/ts/components/Match.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import SuggestionList from "./SuggestionList";
import { getColourForMatch, IMatchTypeToColourMap } from "../utils/decoration";
import { Check } from "@mui/icons-material";
import { getHtmlFromMarkdown } from "../utils/dom";
import { Feedback } from "./Feedback";

interface IMatchProps<TMatch extends IMatch> {
applySuggestions?: (opts: ApplySuggestionOptions) => void;
match: TMatch;
matchColours: IMatchTypeToColourMap;
feedbackHref?: string;
onMarkCorrect?: (match: IMatch) => void;
}

Expand All @@ -25,30 +25,21 @@ class Match<TMatch extends IMatch> extends Component<IMatchProps<TMatch>> {
onMarkCorrect
}: IMatchProps<TMatch> = this.props;
const {
matchId,
category,
message,
suggestions,
replacement,
markAsCorrect,
matchContext,
ruleId
markAsCorrect
} = match;
const url = document.URL;
const feedbackInfo = {
matchId,
category,
message,
suggestions,
replacement,
url,
matchContext,
markAsCorrect,
ruleId
};

// render up to 6 suggestions if they exist (e.g. dictionary rules), otherwise render the replacement (as sometimes exists for classic Typerighter rules)
const suggestionsToRender = suggestions && suggestions.length > 0 ? suggestions.slice(0, 6) : replacement ? [replacement] : [];
const suggestionsToRender =
suggestions && suggestions.length > 0
? suggestions.slice(0, 6)
: replacement
? [replacement]
: [];
const suggestionContent = (
<div className="MatchWidget__suggestion-list">
{suggestionsToRender && applySuggestions && !markAsCorrect && (
Expand Down Expand Up @@ -93,29 +84,12 @@ class Match<TMatch extends IMatch> extends Component<IMatchProps<TMatch>> {
dangerouslySetInnerHTML={{ __html: getHtmlFromMarkdown(message) }}
></div>
<div className="MatchWidget__footer">
{this.props.feedbackHref && (
<div className="MatchWidget__feedbackLink">
<a
target="_blank"
href={this.getFeedbackLink(
this.props.feedbackHref!,
feedbackInfo
)}
>
Issue with this result? Tell us!
</a>
</div>
)}
<Feedback documentUrl={url} match={match} />
</div>
</div>
</div>
);
}

private getFeedbackLink = (feedbackHref: string, feedbackInfo: any) => {
const data = encodeURIComponent(JSON.stringify(feedbackInfo, undefined, 2));
return feedbackHref + data;
};
}

export default Match;
3 changes: 0 additions & 3 deletions src/ts/components/MatchOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ interface IProps {
store: Store;
applySuggestions: (opts: ApplySuggestionOptions) => void;
stopHover: () => void;
feedbackHref?: string;
onMarkCorrect?: (match: IMatch) => void;
}

Expand All @@ -24,7 +23,6 @@ interface IProps {
*/
const matchOverlay = ({
applySuggestions,
feedbackHref,
onMarkCorrect,
stopHover,
store
Expand Down Expand Up @@ -136,7 +134,6 @@ const matchOverlay = ({
match={maybeMatch}
matchColours={pluginState.config.matchColours}
applySuggestions={applySuggestions}
feedbackHref={feedbackHref}
onMarkCorrect={onMarkCorrect}
/>
</div>
Expand Down
3 changes: 0 additions & 3 deletions src/ts/components/createOverlayView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ interface OverlayViewOptions {
store: Store;
commands: Commands;
overlayNode: Element;
feedbackHref?: string;
onMarkCorrect?: (match: IMatch) => void;
telemetryAdapter?: TyperighterTelemetryAdapter;
}
Expand All @@ -28,7 +27,6 @@ export const createOverlayView = ({
telemetryAdapter,
commands,
overlayNode,
feedbackHref,
onMarkCorrect
}: OverlayViewOptions) => {
overlayNode.classList.add("TyperighterPlugin__tooltip-overlay");
Expand Down Expand Up @@ -59,7 +57,6 @@ export const createOverlayView = ({
telemetryAdapter?.matchIsMarkedAsCorrect(match, document.URL);
})
}
feedbackHref={feedbackHref}
stopHover={commands.stopHover}
/>
</TelemetryContext.Provider>,
Expand Down

0 comments on commit f72421f

Please sign in to comment.