Unverified Commit 361f8b59 authored by Andrey Azov's avatar Andrey Azov Committed by GitHub
Browse files

Update contact us form (#544)

- Add the Messages and Contact us elements (tabs) to the header of the communication panel
- Add the disabled state to the Upload component
- Prevent body scroll when the popup is open
- Fix a bug in the Upload component (undefined passed to the callback during file cancellation)
- Close the panel upon a press on the close button
- Allow adding multiple files at once
- Open links to external sites in different tabs
- Remove white space from the fields when validating the form
parent deb0b3dc
Pipeline #183142 passed with stages
in 4 minutes and 47 seconds
...@@ -4,9 +4,10 @@ $offsetTop: 110px; // the distance between the top of the window and the top bor ...@@ -4,9 +4,10 @@ $offsetTop: 110px; // the distance between the top of the window and the top bor
.wrapper { .wrapper {
position: fixed; position: fixed;
width: 100%;
height: 100%;
top: 1px; top: 1px;
bottom: 1px;
left: 0;
right: 0;
z-index: 11; /* Need to go above the top bar */ z-index: 11; /* Need to go above the top bar */
} }
...@@ -44,6 +45,22 @@ $offsetTop: 110px; // the distance between the top of the window and the top bor ...@@ -44,6 +45,22 @@ $offsetTop: 110px; // the distance between the top of the window and the top bor
margin-bottom: 20px; margin-bottom: 20px;
} }
.panelTabs {
grid-area: tabs;
display: flex;
align-items: center;
gap: 30px;
margin-left: 28px;
.tab {
font-size: 12px;
}
.tabDisabled {
color: $medium-dark-grey;
}
}
.panelBody { .panelBody {
grid-area: main; grid-area: main;
padding-top: 30px; padding-top: 30px;
......
...@@ -14,9 +14,11 @@ ...@@ -14,9 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import React from 'react'; import React, { useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { CommunicationPanelContextProvider } from './communicationPanelContext';
import Overlay from 'src/shared/components/overlay/Overlay'; import Overlay from 'src/shared/components/overlay/Overlay';
import CloseButton from 'src/shared/components/close-button/CloseButton'; import CloseButton from 'src/shared/components/close-button/CloseButton';
import ContactUs from './contact-us/ContactUs'; import ContactUs from './contact-us/ContactUs';
...@@ -29,32 +31,65 @@ import styles from './CommunicationPanel.scss'; ...@@ -29,32 +31,65 @@ import styles from './CommunicationPanel.scss';
const CommunicationPanel = () => { const CommunicationPanel = () => {
const showCommunicationPanel = useSelector(isCommunicationPanelOpen); const showCommunicationPanel = useSelector(isCommunicationPanelOpen);
const wrapperRef = useRef<HTMLDivElement>(null);
const panelWrapperRef = useRef<HTMLDivElement>(null);
const panelBodyRef = useRef<HTMLDivElement>(null);
const dispatch = useDispatch(); const dispatch = useDispatch();
if (!showCommunicationPanel) {
return null; useEffect(() => {
} if (showCommunicationPanel) {
wrapperRef.current?.addEventListener('wheel', preventScroll, {
passive: false
});
}
}, [showCommunicationPanel]);
const onClose = () => { const onClose = () => {
dispatch(toggleCommunicationPanel()); dispatch(toggleCommunicationPanel());
}; };
const preventScroll = (e: Event) => {
const eventTarget = e.target as HTMLElement;
if (!panelWrapperRef.current?.contains(eventTarget)) {
// prevent possible window scrolling
e.preventDefault();
e.stopPropagation();
}
};
if (!showCommunicationPanel) {
return null;
}
return ( return (
<div className={styles.wrapper}> <CommunicationPanelContextProvider value={{ panelBody: panelBodyRef }}>
<Overlay className={styles.overlay} /> <div className={styles.wrapper} ref={wrapperRef}>
<div className={styles.panelWrapper}> <Overlay className={styles.overlay} />
<div className={styles.panel}> <div ref={panelWrapperRef} className={styles.panelWrapper}>
<ConversationIcon <div className={styles.panel}>
className={styles.conversationIcon} <ConversationIcon
onClick={onClose} className={styles.conversationIcon}
/> onClick={onClose}
<CloseButton className={styles.panelCloseButton} onClick={onClose} /> />
<div className={styles.panelBody}> {/* TODO: switch to the proper Tabs component when the tabs become functional */}
<ContactUs /> <nav className={styles.panelTabs}>
<span className={`${styles.tab} ${styles.tabDisabled}`}>
Messages
</span>
<span className={styles.tab}>Contact us</span>
</nav>
<CloseButton
className={styles.panelCloseButton}
onClick={onClose}
/>
<div className={styles.panelBody} ref={panelBodyRef}>
<ContactUs />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </CommunicationPanelContextProvider>
); );
}; };
......
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createContext, RefObject } from 'react';
type CommunicationPanelContextType = {
panelBody: RefObject<HTMLDivElement> | null;
};
const initialContext: CommunicationPanelContextType = {
panelBody: null
};
const CommunicationPanelContext =
createContext<CommunicationPanelContextType>(initialContext);
export const { Provider: CommunicationPanelContextProvider } =
CommunicationPanelContext;
export default CommunicationPanelContext;
...@@ -33,7 +33,6 @@ const ContactUs = () => { ...@@ -33,7 +33,6 @@ const ContactUs = () => {
if (shouldShowForm) { if (shouldShowForm) {
return ( return (
<div> <div>
<Invitation />
<Header <Header
title="Send us a message" title="Send us a message"
onClick={() => setShouldShowForm(false)} onClick={() => setShouldShowForm(false)}
...@@ -61,14 +60,26 @@ const ContactUs = () => { ...@@ -61,14 +60,26 @@ const ContactUs = () => {
service status updates. service status updates.
</p> </p>
<p> <p>
<a href="https://www.ensembl.info/"> <a
target="_blank"
rel="noopener noreferrer"
href="https://www.ensembl.info/"
>
<span className={styles.socialMediaLinkText}>Ensembl Blog</span>{' '} <span className={styles.socialMediaLinkText}>Ensembl Blog</span>{' '}
<BlogIcon className={styles.icon} />{' '} <BlogIcon className={styles.icon} />{' '}
</a> </a>
<a href="https://www.facebook.com/Ensembl.org/"> <a
target="_blank"
rel="noopener noreferrer"
href="https://www.facebook.com/Ensembl.org/"
>
<FacebookIcon className={styles.icon} />{' '} <FacebookIcon className={styles.icon} />{' '}
</a> </a>
<a href="https://twitter.com/ensembl"> <a
target="_blank"
rel="noopener noreferrer"
href="https://twitter.com/ensembl"
>
<TwitterIcon className={styles.icon} /> <TwitterIcon className={styles.icon} />
</a> </a>
</p> </p>
......
...@@ -125,8 +125,10 @@ const ContactUsInitialForm = () => { ...@@ -125,8 +125,10 @@ const ContactUsInitialForm = () => {
dispatch({ type: 'update-message', payload: value }); dispatch({ type: 'update-message', payload: value });
}, []); }, []);
const onFileChange = useCallback((file: File) => { const onFileChange = useCallback((fileList: FileList) => {
dispatch({ type: 'add-file', payload: file }); for (const file of fileList) {
dispatch({ type: 'add-file', payload: file });
}
}, []); }, []);
const deleteFile = (index: number) => { const deleteFile = (index: number) => {
...@@ -149,9 +151,10 @@ const ContactUsInitialForm = () => { ...@@ -149,9 +151,10 @@ const ContactUsInitialForm = () => {
<div className={commonStyles.container}> <div className={commonStyles.container}>
<div className={commonStyles.grid}> <div className={commonStyles.grid}>
<p className={commonStyles.advisory}> <p className={commonStyles.advisory}>
<span>All fields are required unless marked optional</span> <span>All fields are required</span>
<span>Second line</span> <span>
<span>Third line</span> The size of your combined attachments should be no more than 10 MB
</span>
</p> </p>
</div> </div>
<form <form
...@@ -200,7 +203,7 @@ const ContactUsInitialForm = () => { ...@@ -200,7 +203,7 @@ const ContactUsInitialForm = () => {
<Upload <Upload
label="Click or drag a file here to upload" label="Click or drag a file here to upload"
callbackWithFiles={true} callbackWithFiles={true}
allowMultiple={false} disabled={!isFormValid}
onChange={onFileChange} onChange={onFileChange}
/> />
</div> </div>
...@@ -230,7 +233,7 @@ const validate = (formState: State) => { ...@@ -230,7 +233,7 @@ const validate = (formState: State) => {
const areMandatoryFieldsFilled = (formState: State) => { const areMandatoryFieldsFilled = (formState: State) => {
return (['name', 'email', 'subject', 'message'] as const).every( return (['name', 'email', 'subject', 'message'] as const).every(
(field) => formState[field] (field) => formState[field].trim().length > 0
); );
}; };
......
.submissionSuccess { .submissionSuccess {
min-height: 570px;
p { p {
margin-bottom: 1rem; margin-bottom: 1rem;
......
...@@ -14,14 +14,31 @@ ...@@ -14,14 +14,31 @@
* limitations under the License. * limitations under the License.
*/ */
import React from 'react'; import React, { useEffect, useContext } from 'react';
import { useDispatch } from 'react-redux';
import CommunicationPanelContext from 'src/shared/components/communication-framework/communicationPanelContext';
import { toggleCommunicationPanel } from 'src/shared/state/communication/communicationSlice';
import { PrimaryButton } from 'src/shared/components/button/Button'; import { PrimaryButton } from 'src/shared/components/button/Button';
import styles from './SubmissionSuccess.scss'; import styles from './SubmissionSuccess.scss';
const SubmissionSuccess = () => { const SubmissionSuccess = () => {
const onButtonClick = () => {}; // eslint-disable-line const dispatch = useDispatch();
const communicationPanelContext = useContext(CommunicationPanelContext);
useEffect(() => {
const { panelBody } = communicationPanelContext;
if (panelBody?.current) {
panelBody.current.scrollTop = 0;
}
}, []);
const onButtonClick = () => {
dispatch(toggleCommunicationPanel());
};
return ( return (
<div className={styles.submissionSuccess}> <div className={styles.submissionSuccess}>
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
position: relative; position: relative;
padding: 15px 27px; padding: 15px 27px;
cursor: pointer; cursor: pointer;
transition: background-color 0.4s; transition: color 0.4s, border-color 0.4s, background-color 0.4s;
} }
.defaultUploadActive { .defaultUploadActive {
...@@ -15,6 +15,15 @@ ...@@ -15,6 +15,15 @@
background-color: rgba(51, 173, 255, 0.2); background-color: rgba(51, 173, 255, 0.2);
} }
.disabledUpload {
border: 1px dashed $grey;
color: $grey;
display: inline-block;
padding: 15px 27px;
user-select: none;
transition: color 0.4s, border-color 0.4s;
}
.fileInput { .fileInput {
opacity: 0; opacity: 0;
overflow: hidden; overflow: hidden;
......
...@@ -73,8 +73,10 @@ export type UploadProps = { ...@@ -73,8 +73,10 @@ export type UploadProps = {
classNames?: { classNames?: {
default?: string; default?: string;
active?: string; active?: string;
disabled?: string;
}; };
allowMultiple?: boolean; allowMultiple?: boolean;
disabled?: boolean;
} & OnChangeProps; } & OnChangeProps;
const Upload = (props: UploadProps) => { const Upload = (props: UploadProps) => {
...@@ -112,12 +114,12 @@ const Upload = (props: UploadProps) => { ...@@ -112,12 +114,12 @@ const Upload = (props: UploadProps) => {
const files: FileList | null = const files: FileList | null =
get(e, 'target.files') || get(e, 'dataTransfer.files') || null; get(e, 'target.files') || get(e, 'dataTransfer.files') || null;
if (!files) { if (!files?.length) {
return; return;
} }
if (props.callbackWithFiles) { if (props.callbackWithFiles) {
// Just consider the first file if allowMultiple is true // Just pass the first file to the callback if allowMultiple is true
if (!props.allowMultiple) { if (!props.allowMultiple) {
props.onChange(files[0]); props.onChange(files[0]);
return; return;
...@@ -169,6 +171,16 @@ const Upload = (props: UploadProps) => { ...@@ -169,6 +171,16 @@ const Upload = (props: UploadProps) => {
return classNames(styles.defaultUploadActive, props.classNames.active); return classNames(styles.defaultUploadActive, props.classNames.active);
}; };
if (props.disabled) {
const elementClasses = classNames(
styles.disabledUpload,
props.classNames?.disabled
);
// using the label html tag even though there is no input
// to keep it the same as the label element of the enabled component (to support animations)
return <label className={elementClasses}>{props.label}</label>;
}
return ( return (
<label <label
className={`${getDefaultClassNames()} ${getActiveClassNames()}`} className={`${getDefaultClassNames()} ${getActiveClassNames()}`}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment