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
.wrapper {
position: fixed;
width: 100%;
height: 100%;
top: 1px;
bottom: 1px;
left: 0;
right: 0;
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
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 {
grid-area: main;
padding-top: 30px;
......
......@@ -14,9 +14,11 @@
* limitations under the License.
*/
import React from 'react';
import React, { useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { CommunicationPanelContextProvider } from './communicationPanelContext';
import Overlay from 'src/shared/components/overlay/Overlay';
import CloseButton from 'src/shared/components/close-button/CloseButton';
import ContactUs from './contact-us/ContactUs';
......@@ -29,32 +31,65 @@ import styles from './CommunicationPanel.scss';
const CommunicationPanel = () => {
const showCommunicationPanel = useSelector(isCommunicationPanelOpen);
const wrapperRef = useRef<HTMLDivElement>(null);
const panelWrapperRef = useRef<HTMLDivElement>(null);
const panelBodyRef = useRef<HTMLDivElement>(null);
const dispatch = useDispatch();
if (!showCommunicationPanel) {
return null;
}
useEffect(() => {
if (showCommunicationPanel) {
wrapperRef.current?.addEventListener('wheel', preventScroll, {
passive: false
});
}
}, [showCommunicationPanel]);
const onClose = () => {
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 (
<div className={styles.wrapper}>
<Overlay className={styles.overlay} />
<div className={styles.panelWrapper}>
<div className={styles.panel}>
<ConversationIcon
className={styles.conversationIcon}
onClick={onClose}
/>
<CloseButton className={styles.panelCloseButton} onClick={onClose} />
<div className={styles.panelBody}>
<ContactUs />
<CommunicationPanelContextProvider value={{ panelBody: panelBodyRef }}>
<div className={styles.wrapper} ref={wrapperRef}>
<Overlay className={styles.overlay} />
<div ref={panelWrapperRef} className={styles.panelWrapper}>
<div className={styles.panel}>
<ConversationIcon
className={styles.conversationIcon}
onClick={onClose}
/>
{/* TODO: switch to the proper Tabs component when the tabs become functional */}
<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>
</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 = () => {
if (shouldShowForm) {
return (
<div>
<Invitation />
<Header
title="Send us a message"
onClick={() => setShouldShowForm(false)}
......@@ -61,14 +60,26 @@ const ContactUs = () => {
service status updates.
</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>{' '}
<BlogIcon className={styles.icon} />{' '}
</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} />{' '}
</a>
<a href="https://twitter.com/ensembl">
<a
target="_blank"
rel="noopener noreferrer"
href="https://twitter.com/ensembl"
>
<TwitterIcon className={styles.icon} />
</a>
</p>
......
......@@ -125,8 +125,10 @@ const ContactUsInitialForm = () => {
dispatch({ type: 'update-message', payload: value });
}, []);
const onFileChange = useCallback((file: File) => {
dispatch({ type: 'add-file', payload: file });
const onFileChange = useCallback((fileList: FileList) => {
for (const file of fileList) {
dispatch({ type: 'add-file', payload: file });
}
}, []);
const deleteFile = (index: number) => {
......@@ -149,9 +151,10 @@ const ContactUsInitialForm = () => {
<div className={commonStyles.container}>
<div className={commonStyles.grid}>
<p className={commonStyles.advisory}>
<span>All fields are required unless marked optional</span>
<span>Second line</span>
<span>Third line</span>
<span>All fields are required</span>
<span>
The size of your combined attachments should be no more than 10 MB
</span>
</p>
</div>
<form
......@@ -200,7 +203,7 @@ const ContactUsInitialForm = () => {
<Upload
label="Click or drag a file here to upload"
callbackWithFiles={true}
allowMultiple={false}
disabled={!isFormValid}
onChange={onFileChange}
/>
</div>
......@@ -230,7 +233,7 @@ const validate = (formState: State) => {
const areMandatoryFieldsFilled = (formState: State) => {
return (['name', 'email', 'subject', 'message'] as const).every(
(field) => formState[field]
(field) => formState[field].trim().length > 0
);
};
......
......@@ -14,14 +14,31 @@
* 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 styles from './SubmissionSuccess.scss';
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 (
<div className={styles.submissionSuccess}>
......
......@@ -7,7 +7,7 @@
position: relative;
padding: 15px 27px;
cursor: pointer;
transition: background-color 0.4s;
transition: color 0.4s, border-color 0.4s, background-color 0.4s;
}
.defaultUploadActive {
......@@ -15,6 +15,15 @@
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 {
opacity: 0;
overflow: hidden;
......
......@@ -73,8 +73,10 @@ export type UploadProps = {
classNames?: {
default?: string;
active?: string;
disabled?: string;
};
allowMultiple?: boolean;
disabled?: boolean;
} & OnChangeProps;
const Upload = (props: UploadProps) => {
......@@ -112,12 +114,12 @@ const Upload = (props: UploadProps) => {
const files: FileList | null =
get(e, 'target.files') || get(e, 'dataTransfer.files') || null;
if (!files) {
if (!files?.length) {
return;
}
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) {
props.onChange(files[0]);
return;
......@@ -169,6 +171,16 @@ const Upload = (props: UploadProps) => {
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 (
<label
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