|
|
|
@ -1,15 +1,13 @@ |
|
|
|
|
import { Theme } from '@mui/material'; |
|
|
|
|
import { ClassNameMap, withStyles } from '@mui/styles'; |
|
|
|
|
import React, { Component } from 'react'; |
|
|
|
|
import { WithTranslation } from 'react-i18next'; |
|
|
|
|
import { connect } from 'react-redux'; |
|
|
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react'; |
|
|
|
|
import { useTranslation } from 'react-i18next'; |
|
|
|
|
import { useDispatch, useSelector } from 'react-redux'; |
|
|
|
|
import { makeStyles } from 'tss-react/mui'; |
|
|
|
|
|
|
|
|
|
import { createFeedbackOpenEvent } from '../../analytics/AnalyticsEvents'; |
|
|
|
|
import { sendAnalytics } from '../../analytics/functions'; |
|
|
|
|
import { IReduxState, IStore } from '../../app/types'; |
|
|
|
|
import { IReduxState } from '../../app/types'; |
|
|
|
|
import { IJitsiConference } from '../../base/conference/reducer'; |
|
|
|
|
import { isMobileBrowser } from '../../base/environment/utils'; |
|
|
|
|
import { translate } from '../../base/i18n/functions'; |
|
|
|
|
import Icon from '../../base/icons/components/Icon'; |
|
|
|
|
import { IconFavorite, IconFavoriteSolid } from '../../base/icons/svg'; |
|
|
|
|
import { withPixelLineHeight } from '../../base/styles/functions.web'; |
|
|
|
@ -17,7 +15,7 @@ import Dialog from '../../base/ui/components/web/Dialog'; |
|
|
|
|
import Input from '../../base/ui/components/web/Input'; |
|
|
|
|
import { cancelFeedback, submitFeedback } from '../actions.web'; |
|
|
|
|
|
|
|
|
|
const styles = (theme: Theme) => { |
|
|
|
|
const useStyles = makeStyles()(theme => { |
|
|
|
|
return { |
|
|
|
|
dialog: { |
|
|
|
|
marginBottom: theme.spacing(1) |
|
|
|
@ -68,7 +66,7 @@ const styles = (theme: Theme) => { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The scores to display for selecting. The score is the index in the array and |
|
|
|
@ -84,31 +82,10 @@ const SCORES = [ |
|
|
|
|
|
|
|
|
|
const ICON_SIZE = 32; |
|
|
|
|
|
|
|
|
|
type Scrollable = { |
|
|
|
|
scroll: Function; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The type of the React {@code Component} props of {@link FeedbackDialog}. |
|
|
|
|
*/ |
|
|
|
|
interface IProps extends WithTranslation { |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The cached feedback message, if any, that was set when closing a previous |
|
|
|
|
* instance of {@code FeedbackDialog}. |
|
|
|
|
*/ |
|
|
|
|
_message: string; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The cached feedback score, if any, that was set when closing a previous |
|
|
|
|
* instance of {@code FeedbackDialog}. |
|
|
|
|
*/ |
|
|
|
|
_score: number; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* An object containing the CSS classes. |
|
|
|
|
*/ |
|
|
|
|
classes: ClassNameMap<string>; |
|
|
|
|
interface IProps { |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The JitsiConference that is being rated. The conference is passed in |
|
|
|
@ -117,11 +94,6 @@ interface IProps extends WithTranslation { |
|
|
|
|
*/ |
|
|
|
|
conference: IJitsiConference; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Invoked to signal feedback submission or canceling. |
|
|
|
|
*/ |
|
|
|
|
dispatch: IStore['dispatch']; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Callback invoked when {@code FeedbackDialog} is unmounted. |
|
|
|
|
*/ |
|
|
|
@ -129,224 +101,67 @@ interface IProps extends WithTranslation { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The type of the React {@code Component} state of {@link FeedbackDialog}. |
|
|
|
|
* A React {@code Component} for displaying a dialog to rate the current |
|
|
|
|
* conference quality, write a message describing the experience, and submit |
|
|
|
|
* the feedback. |
|
|
|
|
* |
|
|
|
|
* @param {IProps} props - Component's props. |
|
|
|
|
* @returns {JSX} |
|
|
|
|
*/ |
|
|
|
|
interface IState { |
|
|
|
|
const FeedbackDialog = ({ conference, onClose }: IProps) => { |
|
|
|
|
const { classes } = useStyles(); |
|
|
|
|
const dispatch = useDispatch(); |
|
|
|
|
const { t } = useTranslation(); |
|
|
|
|
const _message = useSelector((state: IReduxState) => state['features/feedback'].message); |
|
|
|
|
const _score = useSelector((state: IReduxState) => state['features/feedback'].score); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The currently entered feedback message. |
|
|
|
|
*/ |
|
|
|
|
message: string; |
|
|
|
|
const [ message, setMessage ] = useState(_message); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The score selection index which is currently being hovered. The value -1 |
|
|
|
|
* is used as a sentinel value to match store behavior of using -1 for no |
|
|
|
|
* score having been selected. |
|
|
|
|
* The score selection index which is currently being hovered. The |
|
|
|
|
* value -1 is used as a sentinel value to match store behavior of |
|
|
|
|
* using -1 for no score having been selected. |
|
|
|
|
*/ |
|
|
|
|
mousedOverScore: number; |
|
|
|
|
const [ mousedOverScore, setMousedOverScore ] = useState(-1); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The currently selected score selection index. The score will not be 0 |
|
|
|
|
* indexed so subtract one to map with SCORES. |
|
|
|
|
* The currently selected score selection index. The score will not |
|
|
|
|
* be 0 indexed so subtract one to map with SCORES. |
|
|
|
|
*/ |
|
|
|
|
score: number; |
|
|
|
|
} |
|
|
|
|
const [ score, setScore ] = useState(_score > -1 ? _score - 1 : _score); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* A React {@code Component} for displaying a dialog to rate the current |
|
|
|
|
* conference quality, write a message describing the experience, and submit |
|
|
|
|
* the feedback. |
|
|
|
|
* |
|
|
|
|
* @augments Component |
|
|
|
|
*/ |
|
|
|
|
class FeedbackDialog extends Component<IProps, IState> { |
|
|
|
|
/** |
|
|
|
|
* An array of objects with click handlers for each of the scores listed in |
|
|
|
|
* the constant SCORES. This pattern is used for binding event handlers only |
|
|
|
|
* once for each score selection icon. |
|
|
|
|
*/ |
|
|
|
|
_scoreClickConfigurations: Array<{ |
|
|
|
|
_onClick: (e: React.MouseEvent) => void; |
|
|
|
|
_onKeyDown: (e: React.KeyboardEvent) => void; |
|
|
|
|
_onMouseOver: (e: React.MouseEvent) => void; |
|
|
|
|
}>; |
|
|
|
|
|
|
|
|
|
_onScrollTop: (node: Scrollable | null) => void; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Initializes a new {@code FeedbackDialog} instance. |
|
|
|
|
* |
|
|
|
|
* @param {Object} props - The read-only React {@code Component} props with |
|
|
|
|
* which the new instance is to be initialized. |
|
|
|
|
*/ |
|
|
|
|
constructor(props: IProps) { |
|
|
|
|
super(props); |
|
|
|
|
|
|
|
|
|
const { _message, _score } = this.props; |
|
|
|
|
|
|
|
|
|
this.state = { |
|
|
|
|
/** |
|
|
|
|
* The currently entered feedback message. |
|
|
|
|
* |
|
|
|
|
* @type {string} |
|
|
|
|
*/ |
|
|
|
|
message: _message, |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The score selection index which is currently being hovered. The |
|
|
|
|
* value -1 is used as a sentinel value to match store behavior of |
|
|
|
|
* using -1 for no score having been selected. |
|
|
|
|
* |
|
|
|
|
* @type {number} |
|
|
|
|
*/ |
|
|
|
|
mousedOverScore: -1, |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The currently selected score selection index. The score will not |
|
|
|
|
* be 0 indexed so subtract one to map with SCORES. |
|
|
|
|
* |
|
|
|
|
* @type {number} |
|
|
|
|
*/ |
|
|
|
|
score: _score > -1 ? _score - 1 : _score |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
this._scoreClickConfigurations = SCORES.map((textKey, index) => { |
|
|
|
|
return { |
|
|
|
|
_onClick: () => this._onScoreSelect(index), |
|
|
|
|
_onKeyDown: (e: React.KeyboardEvent) => { |
|
|
|
|
if (e.key === ' ' || e.key === 'Enter') { |
|
|
|
|
e.stopPropagation(); |
|
|
|
|
e.preventDefault(); |
|
|
|
|
this._onScoreSelect(index); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
_onMouseOver: () => this._onScoreMouseOver(index) |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Bind event handlers so they are only bound once for every instance.
|
|
|
|
|
this._onCancel = this._onCancel.bind(this); |
|
|
|
|
this._onMessageChange = this._onMessageChange.bind(this); |
|
|
|
|
this._onScoreContainerMouseLeave |
|
|
|
|
= this._onScoreContainerMouseLeave.bind(this); |
|
|
|
|
this._onSubmit = this._onSubmit.bind(this); |
|
|
|
|
|
|
|
|
|
// On some mobile browsers opening Feedback dialog scrolls down the whole content because of the keyboard.
|
|
|
|
|
// By scrolling to the top we prevent hiding the feedback stars so the user knows those exist.
|
|
|
|
|
this._onScrollTop = (node: Scrollable | null) => { |
|
|
|
|
node?.scroll?.(0, 0); |
|
|
|
|
const scoreClickConfigurations = useRef(SCORES.map((textKey, index) => { |
|
|
|
|
return { |
|
|
|
|
_onClick: () => onScoreSelect(index), |
|
|
|
|
_onKeyDown: (e: React.KeyboardEvent) => { |
|
|
|
|
if (e.key === ' ' || e.key === 'Enter') { |
|
|
|
|
e.stopPropagation(); |
|
|
|
|
e.preventDefault(); |
|
|
|
|
onScoreSelect(index); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
_onMouseOver: () => onScoreMouseOver(index) |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
})); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Emits an analytics event to notify feedback has been opened. |
|
|
|
|
* |
|
|
|
|
* @inheritdoc |
|
|
|
|
*/ |
|
|
|
|
componentDidMount() { |
|
|
|
|
useEffect(() => { |
|
|
|
|
sendAnalytics(createFeedbackOpenEvent()); |
|
|
|
|
if (typeof APP !== 'undefined') { |
|
|
|
|
APP.API.notifyFeedbackPromptDisplayed(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Invokes the onClose callback, if defined, to notify of the close event. |
|
|
|
|
* |
|
|
|
|
* @inheritdoc |
|
|
|
|
*/ |
|
|
|
|
componentWillUnmount() { |
|
|
|
|
if (this.props.onClose) { |
|
|
|
|
this.props.onClose(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Implements React's {@link Component#render()}. |
|
|
|
|
* |
|
|
|
|
* @inheritdoc |
|
|
|
|
* @returns {ReactElement} |
|
|
|
|
*/ |
|
|
|
|
render() { |
|
|
|
|
const { message, mousedOverScore, score } = this.state; |
|
|
|
|
const scoreToDisplayAsSelected |
|
|
|
|
= mousedOverScore > -1 ? mousedOverScore : score; |
|
|
|
|
|
|
|
|
|
const { classes, t } = this.props; |
|
|
|
|
|
|
|
|
|
const scoreIcons = this._scoreClickConfigurations.map( |
|
|
|
|
(config, index) => { |
|
|
|
|
const isFilled = index <= scoreToDisplayAsSelected; |
|
|
|
|
const activeClass = isFilled ? 'active' : ''; |
|
|
|
|
const className |
|
|
|
|
= `${classes.starBtn} ${activeClass}`; |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<span |
|
|
|
|
aria-label = { t(SCORES[index]) } |
|
|
|
|
className = { className } |
|
|
|
|
key = { index } |
|
|
|
|
onClick = { config._onClick } |
|
|
|
|
onKeyDown = { config._onKeyDown } |
|
|
|
|
role = 'button' |
|
|
|
|
tabIndex = { 0 } |
|
|
|
|
{ ...(isMobileBrowser() ? {} : { |
|
|
|
|
onMouseOver: config._onMouseOver |
|
|
|
|
}) }> |
|
|
|
|
{ isFilled |
|
|
|
|
? <Icon |
|
|
|
|
size = { ICON_SIZE } |
|
|
|
|
src = { IconFavoriteSolid } /> |
|
|
|
|
: <Icon |
|
|
|
|
size = { ICON_SIZE } |
|
|
|
|
src = { IconFavorite } /> } |
|
|
|
|
</span> |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<Dialog |
|
|
|
|
ok = {{ |
|
|
|
|
translationKey: 'dialog.Submit' |
|
|
|
|
}} |
|
|
|
|
onCancel = { this._onCancel } |
|
|
|
|
onSubmit = { this._onSubmit } |
|
|
|
|
size = 'large' |
|
|
|
|
titleKey = 'feedback.rateExperience'> |
|
|
|
|
<div className = { classes.dialog }> |
|
|
|
|
<div className = { classes.rating }> |
|
|
|
|
<div |
|
|
|
|
className = { classes.stars } |
|
|
|
|
onMouseLeave = { this._onScoreContainerMouseLeave }> |
|
|
|
|
{ scoreIcons } |
|
|
|
|
</div> |
|
|
|
|
<div |
|
|
|
|
className = { classes.ratingLabel } > |
|
|
|
|
<p className = 'sr-only'> |
|
|
|
|
{ t('feedback.accessibilityLabel.yourChoice', { |
|
|
|
|
rating: t(SCORES[scoreToDisplayAsSelected]) |
|
|
|
|
}) } |
|
|
|
|
</p> |
|
|
|
|
<p |
|
|
|
|
aria-hidden = { true } |
|
|
|
|
id = 'starLabel'> |
|
|
|
|
{ t(SCORES[scoreToDisplayAsSelected]) } |
|
|
|
|
</p> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
<div className = { classes.details }> |
|
|
|
|
<Input |
|
|
|
|
id = 'feedbackTextArea' |
|
|
|
|
label = { t('feedback.detailsLabel') } |
|
|
|
|
onChange = { this._onMessageChange } |
|
|
|
|
textarea = { true } |
|
|
|
|
value = { message } /> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</Dialog> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
return () => { |
|
|
|
|
onClose?.(); |
|
|
|
|
}; |
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Dispatches an action notifying feedback was not submitted. The submitted |
|
|
|
@ -356,14 +171,13 @@ class FeedbackDialog extends Component<IProps, IState> { |
|
|
|
|
* @private |
|
|
|
|
* @returns {boolean} Returns true to close the dialog. |
|
|
|
|
*/ |
|
|
|
|
_onCancel() { |
|
|
|
|
const { message, score } = this.state; |
|
|
|
|
const onCancel = useCallback(() => { |
|
|
|
|
const scoreToSubmit = score > -1 ? score + 1 : score; |
|
|
|
|
|
|
|
|
|
this.props.dispatch(cancelFeedback(scoreToSubmit, message)); |
|
|
|
|
dispatch(cancelFeedback(scoreToSubmit, message)); |
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
}, [ score, message ]); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Updates the known entered feedback message. |
|
|
|
@ -373,19 +187,19 @@ class FeedbackDialog extends Component<IProps, IState> { |
|
|
|
|
* @private |
|
|
|
|
* @returns {void} |
|
|
|
|
*/ |
|
|
|
|
_onMessageChange(newValue: string) { |
|
|
|
|
this.setState({ message: newValue }); |
|
|
|
|
} |
|
|
|
|
const onMessageChange = useCallback((newValue: string) => { |
|
|
|
|
setMessage(newValue); |
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Updates the currently selected score. |
|
|
|
|
* |
|
|
|
|
* @param {number} score - The index of the selected score in SCORES. |
|
|
|
|
* @param {number} newScore - The index of the selected score in SCORES. |
|
|
|
|
* @private |
|
|
|
|
* @returns {void} |
|
|
|
|
*/ |
|
|
|
|
_onScoreSelect(score: number) { |
|
|
|
|
this.setState({ score }); |
|
|
|
|
function onScoreSelect(newScore: number) { |
|
|
|
|
setScore(newScore); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
@ -395,20 +209,20 @@ class FeedbackDialog extends Component<IProps, IState> { |
|
|
|
|
* @private |
|
|
|
|
* @returns {void} |
|
|
|
|
*/ |
|
|
|
|
_onScoreContainerMouseLeave() { |
|
|
|
|
this.setState({ mousedOverScore: -1 }); |
|
|
|
|
} |
|
|
|
|
const onScoreContainerMouseLeave = useCallback(() => { |
|
|
|
|
setMousedOverScore(-1); |
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Updates the known state of the score icon currently behind hovered over. |
|
|
|
|
* |
|
|
|
|
* @param {number} mousedOverScore - The index of the SCORES value currently |
|
|
|
|
* @param {number} newMousedOverScore - The index of the SCORES value currently |
|
|
|
|
* being moused over. |
|
|
|
|
* @private |
|
|
|
|
* @returns {void} |
|
|
|
|
*/ |
|
|
|
|
_onScoreMouseOver(mousedOverScore: number) { |
|
|
|
|
this.setState({ mousedOverScore }); |
|
|
|
|
function onScoreMouseOver(newMousedOverScore: number) { |
|
|
|
|
setMousedOverScore(newMousedOverScore); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
@ -418,46 +232,89 @@ class FeedbackDialog extends Component<IProps, IState> { |
|
|
|
|
* @private |
|
|
|
|
* @returns {boolean} Returns true to close the dialog. |
|
|
|
|
*/ |
|
|
|
|
_onSubmit() { |
|
|
|
|
const { conference, dispatch } = this.props; |
|
|
|
|
const { message, score } = this.state; |
|
|
|
|
|
|
|
|
|
const _onSubmit = useCallback(() => { |
|
|
|
|
const scoreToSubmit = score > -1 ? score + 1 : score; |
|
|
|
|
|
|
|
|
|
dispatch(submitFeedback(scoreToSubmit, message, conference)); |
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}, [ score, message, conference ]); |
|
|
|
|
|
|
|
|
|
const scoreToDisplayAsSelected |
|
|
|
|
= mousedOverScore > -1 ? mousedOverScore : score; |
|
|
|
|
|
|
|
|
|
const scoreIcons = scoreClickConfigurations.current.map( |
|
|
|
|
(config, index) => { |
|
|
|
|
const isFilled = index <= scoreToDisplayAsSelected; |
|
|
|
|
const activeClass = isFilled ? 'active' : ''; |
|
|
|
|
const className |
|
|
|
|
= `${classes.starBtn} ${activeClass}`; |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<span |
|
|
|
|
aria-label = { t(SCORES[index]) } |
|
|
|
|
className = { className } |
|
|
|
|
key = { index } |
|
|
|
|
onClick = { config._onClick } |
|
|
|
|
onKeyDown = { config._onKeyDown } |
|
|
|
|
role = 'button' |
|
|
|
|
tabIndex = { 0 } |
|
|
|
|
{ ...(isMobileBrowser() ? {} : { |
|
|
|
|
onMouseOver: config._onMouseOver |
|
|
|
|
}) }> |
|
|
|
|
{isFilled |
|
|
|
|
? <Icon |
|
|
|
|
size = { ICON_SIZE } |
|
|
|
|
src = { IconFavoriteSolid } /> |
|
|
|
|
: <Icon |
|
|
|
|
size = { ICON_SIZE } |
|
|
|
|
src = { IconFavorite } />} |
|
|
|
|
</span> |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Maps (parts of) the Redux state to the associated {@code FeedbackDialog}'s |
|
|
|
|
* props. |
|
|
|
|
* |
|
|
|
|
* @param {Object} state - The Redux state. |
|
|
|
|
* @private |
|
|
|
|
* @returns {{ |
|
|
|
|
* }} |
|
|
|
|
*/ |
|
|
|
|
function _mapStateToProps(state: IReduxState) { |
|
|
|
|
const { message, score } = state['features/feedback']; |
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
/** |
|
|
|
|
* The cached feedback message, if any, that was set when closing a |
|
|
|
|
* previous instance of {@code FeedbackDialog}. |
|
|
|
|
* |
|
|
|
|
* @type {string} |
|
|
|
|
*/ |
|
|
|
|
_message: message, |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* The currently selected score selection index. |
|
|
|
|
* |
|
|
|
|
* @type {number} |
|
|
|
|
*/ |
|
|
|
|
_score: score |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
return ( |
|
|
|
|
<Dialog |
|
|
|
|
ok = {{ |
|
|
|
|
translationKey: 'dialog.Submit' |
|
|
|
|
}} |
|
|
|
|
onCancel = { onCancel } |
|
|
|
|
onSubmit = { _onSubmit } |
|
|
|
|
size = 'large' |
|
|
|
|
titleKey = 'feedback.rateExperience'> |
|
|
|
|
<div className = { classes.dialog }> |
|
|
|
|
<div className = { classes.rating }> |
|
|
|
|
<div |
|
|
|
|
className = { classes.stars } |
|
|
|
|
onMouseLeave = { onScoreContainerMouseLeave }> |
|
|
|
|
{scoreIcons} |
|
|
|
|
</div> |
|
|
|
|
<div |
|
|
|
|
className = { classes.ratingLabel } > |
|
|
|
|
<p className = 'sr-only'> |
|
|
|
|
{t('feedback.accessibilityLabel.yourChoice', { |
|
|
|
|
rating: t(SCORES[scoreToDisplayAsSelected]) |
|
|
|
|
})} |
|
|
|
|
</p> |
|
|
|
|
<p |
|
|
|
|
aria-hidden = { true } |
|
|
|
|
id = 'starLabel'> |
|
|
|
|
{t(SCORES[scoreToDisplayAsSelected])} |
|
|
|
|
</p> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
<div className = { classes.details }> |
|
|
|
|
<Input |
|
|
|
|
id = 'feedbackTextArea' |
|
|
|
|
label = { t('feedback.detailsLabel') } |
|
|
|
|
onChange = { onMessageChange } |
|
|
|
|
textarea = { true } |
|
|
|
|
value = { message } /> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</Dialog> |
|
|
|
|
); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
export default withStyles(styles)(translate(connect(_mapStateToProps)(FeedbackDialog))); |
|
|
|
|
export default FeedbackDialog; |
|
|
|
|