mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 05:00:43 +10:00
Merge pull request #550 from sharetribe/send-message-form
SendMessageForm component
This commit is contained in:
commit
df81c74e46
9 changed files with 288 additions and 6 deletions
|
|
@ -6,20 +6,29 @@ class ExpandingTextarea extends Component {
|
|||
super(props);
|
||||
this.timeoutId = null;
|
||||
this.textarea = null;
|
||||
this.update = this.update.bind(this);
|
||||
}
|
||||
componentDidMount() {
|
||||
// Delay the autosize initialisation so that the autosize can
|
||||
// correctly calculate the height with the textarea value
|
||||
this.timeoutId = window.setTimeout(() => {
|
||||
autosize(this.textarea);
|
||||
|
||||
// Listen to resize events so autosize can pick up updated CSS
|
||||
// values (like max-height) when breakpoints change.
|
||||
window.addEventListener('resize', this.update);
|
||||
}, 100);
|
||||
}
|
||||
componentDidUpdate() {
|
||||
autosize.update(this.textarea);
|
||||
this.update();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
autosize.destroy(this.textarea);
|
||||
window.clearTimeout(this.timeoutId);
|
||||
window.removeEventListener('resize', this.update);
|
||||
}
|
||||
update() {
|
||||
autosize.update(this.textarea);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class TextInputFieldComponent extends Component {
|
|||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
inputRootClass,
|
||||
clearOnUnmount,
|
||||
customErrorText,
|
||||
id,
|
||||
|
|
@ -43,11 +44,13 @@ class TextInputFieldComponent extends Component {
|
|||
|
||||
const fieldMeta = { touched, error: errorText };
|
||||
|
||||
const inputClasses = classNames(css.input, {
|
||||
[css.inputSuccess]: valid,
|
||||
[css.inputError]: hasError,
|
||||
[css.textarea]: isTextarea,
|
||||
});
|
||||
const inputClasses =
|
||||
inputRootClass ||
|
||||
classNames(css.input, {
|
||||
[css.inputSuccess]: valid,
|
||||
[css.inputError]: hasError,
|
||||
[css.textarea]: isTextarea,
|
||||
});
|
||||
const inputProps = isTextarea
|
||||
? { className: inputClasses, id, ...input, ...rest }
|
||||
: { className: inputClasses, id, type, ...input, ...rest };
|
||||
|
|
@ -66,6 +69,7 @@ class TextInputFieldComponent extends Component {
|
|||
TextInputFieldComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
inputRootClass: null,
|
||||
clearOnUnmount: false,
|
||||
customErrorText: null,
|
||||
id: null,
|
||||
|
|
@ -77,6 +81,7 @@ const { string, bool, shape, func, object } = PropTypes;
|
|||
TextInputFieldComponent.propTypes = {
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
inputRootClass: string,
|
||||
|
||||
clearOnUnmount: bool,
|
||||
|
||||
|
|
|
|||
82
src/containers/SendMessageForm/SendMessageForm.css
Normal file
82
src/containers/SendMessageForm/SendMessageForm.css
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
box-shadow: var(--boxShadowBottomForm);
|
||||
|
||||
@media (--viewportLarge) {
|
||||
box-shadow: none;
|
||||
|
||||
/* Clearfix */
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea {
|
||||
@apply --marketplaceH4FontStyles;
|
||||
border-bottom-width: 0;
|
||||
|
||||
/* Avoid text going behind the submit button */
|
||||
padding: 22px 76px 25px 24px;
|
||||
|
||||
margin: 0;
|
||||
max-height: 183px;
|
||||
|
||||
background-color: var(--matterColorLight);
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 0;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
padding: 0 0 5px 0;
|
||||
margin: 0;
|
||||
max-height: initial;
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-color: var(--attentionColor);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.submitButtonMobile {
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
right: 20px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fillSuccess {
|
||||
fill: var(--successColor);
|
||||
}
|
||||
|
||||
.strokeMatter {
|
||||
stroke: var(--matterColor);
|
||||
}
|
||||
|
||||
.submitButtonDesktop {
|
||||
@apply --marketplaceH5FontStyles;
|
||||
font-weight: var(--fontWeightMedium);
|
||||
display: none;
|
||||
|
||||
float: right;
|
||||
padding: 0 16px;
|
||||
min-height: auto;
|
||||
height: 41px;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: inline-block;
|
||||
margin: 17px 0 0 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sendIconDesktop {
|
||||
margin: -3px 5px 0 0;
|
||||
}
|
||||
21
src/containers/SendMessageForm/SendMessageForm.example.js
Normal file
21
src/containers/SendMessageForm/SendMessageForm.example.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import SendMessageForm from './SendMessageForm';
|
||||
|
||||
export const Empty = {
|
||||
component: SendMessageForm,
|
||||
props: {
|
||||
messagePlaceholder: 'Send message to Juho…',
|
||||
onChange: values => {
|
||||
console.log('values changed to:', values);
|
||||
},
|
||||
onSubmit: values => {
|
||||
console.log('submit values:', values);
|
||||
},
|
||||
onFocus: () => {
|
||||
console.log('focus on message form');
|
||||
},
|
||||
onBlur: () => {
|
||||
console.log('blur on message form');
|
||||
},
|
||||
},
|
||||
group: 'forms',
|
||||
};
|
||||
160
src/containers/SendMessageForm/SendMessageForm.js
Normal file
160
src/containers/SendMessageForm/SendMessageForm.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import React, { Component } from 'react';
|
||||
import { string, bool, func } from 'prop-types';
|
||||
import { compose } from 'redux';
|
||||
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
||||
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
|
||||
import classNames from 'classnames';
|
||||
import { Form, TextInputField, SecondaryButton } from '../../components';
|
||||
|
||||
import css from './SendMessageForm.css';
|
||||
|
||||
const BLUR_TIMEOUT_MS = 100;
|
||||
|
||||
const IconSendMessageMobile = () => {
|
||||
return (
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter
|
||||
x="-1.9%"
|
||||
y="-8.5%"
|
||||
width="103.7%"
|
||||
height="113.2%"
|
||||
filterUnits="objectBoundingBox"
|
||||
id="a"
|
||||
>
|
||||
<feOffset dy="-2" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||
<feGaussianBlur stdDeviation="2" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||
in="shadowBlurOuter1"
|
||||
result="shadowMatrixOuter1"
|
||||
/>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="translate(4 6)" filter="url(#a)" fill="none" fillRule="evenodd">
|
||||
<rect className={css.fillSuccess} width="48" height="48" rx="24" />
|
||||
<g fill="#FFF">
|
||||
<path d="M14.47 23.048c-.14.05-.237.193-.25.36-.013.163.062.317.19.39l4.623 2.688 12.162-10.593-16.726 7.155zM20.47 27.327l-.97 6.59c0 .228.184.416.417.416.145 0 .284-.076.36-.206l2.94-4.823 4.833 2.894c.118.066.26.067.373.015.12-.055.207-.162.234-.292l3.315-15.328-11.5 10.735z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const IconSendMessageDesktop = () => {
|
||||
return (
|
||||
<svg
|
||||
className={css.sendIconDesktop}
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g className={css.strokeMatter} fill="none" fillRule="evenodd" strokeLinejoin="round">
|
||||
<path d="M12.91 1L0 7.003l5.052 2.212z" />
|
||||
<path d="M10.75 11.686L5.042 9.222l7.928-8.198z" />
|
||||
<path d="M5.417 8.583v4.695l2.273-2.852" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
class SendMessageFormComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleBlur = this.handleBlur.bind(this);
|
||||
this.blurTimeoutId = null;
|
||||
}
|
||||
handleFocus() {
|
||||
this.props.onFocus();
|
||||
window.clearTimeout(this.blurTimeoutId);
|
||||
}
|
||||
handleBlur() {
|
||||
// We only trigger a blur if another focus event doesn't come
|
||||
// within a timeout. This enables keeping the focus synced when
|
||||
// focus is switched between the message area and the submit
|
||||
// button.
|
||||
this.blurTimeoutId = window.setTimeout(() => {
|
||||
this.props.onBlur();
|
||||
}, BLUR_TIMEOUT_MS);
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
rootClassName,
|
||||
className,
|
||||
messagePlaceholder,
|
||||
form,
|
||||
handleSubmit,
|
||||
submitting,
|
||||
inProgress,
|
||||
invalid,
|
||||
} = this.props;
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
const submitInProgress = submitting || inProgress;
|
||||
const submitDisabled = invalid || submitInProgress;
|
||||
|
||||
return (
|
||||
<Form className={classes} onSubmit={handleSubmit}>
|
||||
<TextInputField
|
||||
inputRootClass={css.textarea}
|
||||
type="textarea"
|
||||
id={`${form}.message`}
|
||||
name="message"
|
||||
placeholder={messagePlaceholder}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
/>
|
||||
<button className={css.submitButtonMobile}>
|
||||
<IconSendMessageMobile />
|
||||
</button>
|
||||
<SecondaryButton
|
||||
className={css.submitButtonDesktop}
|
||||
inProgress={submitInProgress}
|
||||
disabled={submitDisabled}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
<IconSendMessageDesktop />
|
||||
<FormattedMessage id="SendMessageForm.sendMessage" />
|
||||
</SecondaryButton>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SendMessageFormComponent.defaultProps = {
|
||||
rootClassName: null,
|
||||
className: null,
|
||||
inProgress: false,
|
||||
messagePlaceholder: null,
|
||||
onFocus: () => null,
|
||||
onBlur: () => null,
|
||||
};
|
||||
|
||||
SendMessageFormComponent.propTypes = {
|
||||
...formPropTypes,
|
||||
rootClassName: string,
|
||||
className: string,
|
||||
inProgress: bool,
|
||||
|
||||
messagePlaceholder: string,
|
||||
onFocus: func,
|
||||
onBlur: func,
|
||||
|
||||
// from injectIntl
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const defaultFormName = 'SendMessageForm';
|
||||
|
||||
const SendMessageForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
|
||||
SendMessageFormComponent
|
||||
);
|
||||
|
||||
export default SendMessageForm;
|
||||
|
|
@ -37,6 +37,7 @@ export { default as ProfileSettingsForm } from './ProfileSettingsForm/ProfileSet
|
|||
export { default as ProfileSettingsPage } from './ProfileSettingsPage/ProfileSettingsPage';
|
||||
export { default as SalePage } from './SalePage/SalePage';
|
||||
export { default as SearchPage } from './SearchPage/SearchPage';
|
||||
export { default as SendMessageForm } from './SendMessageForm/SendMessageForm';
|
||||
export { default as SignupForm } from './SignupForm/SignupForm';
|
||||
export { default as StaticPage } from './StaticPage/StaticPage';
|
||||
export { default as StripePaymentForm } from './StripePaymentForm/StripePaymentForm';
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import * as LoginForm from './containers/LoginForm/LoginForm.example';
|
|||
import * as PasswordRecoveryForm from './containers/PasswordRecoveryForm/PasswordRecoveryForm.example';
|
||||
import * as PasswordResetForm from './containers/PasswordResetForm/PasswordResetForm.example';
|
||||
import * as PayoutDetailsForm from './containers/PayoutDetailsForm/PayoutDetailsForm.example';
|
||||
import * as SendMessageForm from './containers/SendMessageForm/SendMessageForm.example';
|
||||
import * as SignupForm from './containers/SignupForm/SignupForm.example';
|
||||
import * as StripePaymentForm from './containers/StripePaymentForm/StripePaymentForm.example';
|
||||
import * as Typography from './containers/StyleguidePage/Typography.example';
|
||||
|
|
@ -108,6 +109,7 @@ export {
|
|||
PayoutDetailsForm,
|
||||
ResponsiveImage,
|
||||
SelectField,
|
||||
SendMessageForm,
|
||||
SignupForm,
|
||||
StripeBankAccountTokenInputField,
|
||||
StripePaymentForm,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
--boxShadowListingCard: 0 0 50px 0 rgba(0, 0, 0, 0.1);
|
||||
--boxShadowNotFoundPageSearch: 0 2px 4px 0 rgba(0, 0, 0, 0.5);
|
||||
--boxShadowSectionLocationHover: 0 10px 30px 0 rgba(0, 0, 0, 0.1);
|
||||
--boxShadowBottomForm: 0 -2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* ================ Z-index base levels ================ */
|
||||
|
||||
|
|
|
|||
|
|
@ -470,6 +470,7 @@
|
|||
"SectionLocations.listingsInLocation": "Saunas in {location}",
|
||||
"SectionLocations.subtitle": "We have more than 1000 saunas around Finland. Here are some of our most popular locations.",
|
||||
"SectionLocations.title": "We have wooden saunas, electric saunas and even tent saunas.",
|
||||
"SendMessageForm.sendMessage": "Send message",
|
||||
"SignupForm.emailInvalid": "A valid email address is required",
|
||||
"SignupForm.emailLabel": "Email",
|
||||
"SignupForm.emailPlaceholder": "john.doe@example.com",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue