Commit a34ff190 authored by David Björkheim's avatar David Björkheim
Browse files

DEICH-5554 Deichman.no: imporved modal handling

parent a3b14df1
import { Button, Modal } from "@digibib/deichman-ui";
import FocusTrap from "focus-trap-react";
import React, { useEffect } from "react";
import { useState } from "react";
import LoginForm from "../LoginForm";
import ModalPortal from "../ModalPortal/ModalPortal";
export default function LoginButton(props) {
const children = props.children;
const butttonsProps = { ...props };
delete butttonsProps.children;
const [showLoginModal, setShowLoginModal] = useState(false);
const [showDelayedModal, setShowDelayedModal] = useState(false);
useEffect(
() => {
setTimeout(() => setShowDelayedModal(showLoginModal), 300);
},
[showLoginModal]
);
return (
<>
{showLoginModal && (
<ModalPortal>
<FocusTrap
active={showDelayedModal}
focusTrapOptions={{
initialFocus: "#login-trap"
}}
>
<Modal
name="Login"
visible={showDelayedModal}
onClose={() => setShowLoginModal(false)}
showClose
>
<div className="focus-trap-target" id="login-trap" tabIndex="-1">
<LoginForm
title="Logg inn"
fullWidth
preventRedirect
fetchFavouritesOnLogin
fetchKohaBranchesOnLogin
showCancel
onCancel={() => setShowLoginModal(false)}
/>
</div>
</Modal>
</FocusTrap>
</ModalPortal>
)}
<Button {...butttonsProps} onClick={() => setShowLoginModal(true)}>
{children}
</Button>
</>
);
}
import React, { useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import { setBodyFreeze } from "../../utilities/accessibility";
export default function ModalPortal({ children }) {
const el = useRef(document.createElement("div"));
useEffect(() => {
const modalRoot = document.getElementById("modalRoot");
modalRoot.appendChild(el.current);
setBodyFreeze(true);
return () => {
setBodyFreeze(false);
modalRoot && el.current && modalRoot.removeChild(el.current);
};
}, []);
return <>{ReactDOM.createPortal(children, el.current)}</>;
}
import React, { Fragment } from "react";
import PropTypes from "prop-types";
import LoginForm from "../LoginForm";
import { Block, Button, Input, Hr } from "@digibib/deichman-ui";
const RemoteReservationForm = ({
isLoggedIn,
onSubmit,
onClose,
completed,
......@@ -15,27 +12,8 @@ const RemoteReservationForm = ({
borrowerId,
onChangeBorrowerId,
numHolds,
recordId,
publicationTitle
publication = {}
}) => {
// Return login form if not authorised
if (!isLoggedIn) {
return (
<LoginForm
title="Logg inn for å reservere"
fullWidth
preventRedirect
fetchFavouritesOnLogin
redirectModalData={{
type: "reservation",
recordId
}}
showCancel
onCancel={onClose}
/>
);
}
// Return success or error message if reservation completed
if (completed) {
const header = error
......@@ -66,6 +44,7 @@ const RemoteReservationForm = ({
);
}
const { mainTitle } = publication;
return (
<form onSubmit={onSubmit}>
<Block top={4}>
......@@ -74,7 +53,7 @@ const RemoteReservationForm = ({
<Block top={6}>
<p>
<strong>{publicationTitle}</strong>
<strong>{mainTitle}</strong>
</p>
{numHolds > 0 && (
<p>
......
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import "./styles.css";
import LoginForm from "../LoginForm";
import { Block, Button, Select, Hr } from "@digibib/deichman-ui";
import { translations as TRANSLATIONS } from "../../constants/translations";
import PublicationImage from "../PublicationImage";
import { getWaitingPeriodText } from "../../utilities/datetime";
import { sortBy } from "lodash";
const ReservationForm = ({
isLoggedIn,
import "./styles.css";
export default function ReservationForm({
allBranches,
selectedBranchId,
onChangeBranch,
......@@ -23,36 +21,7 @@ const ReservationForm = ({
error,
reservationWork,
onRedirectClose
}) => {
// Return login form if not authorised
if (!isLoggedIn) {
return (
<LoginForm
title="Logg inn for å reservere"
fullWidth
preventRedirect
fetchFavouritesOnLogin
fetchKohaBranchesOnLogin
showCancel
onCancel={onClose}
/>
);
}
const renderQueueInformation = () => {
const currentItem = reservationWork?.currentItem;
const priority = currentItem?.pri;
const estimate = currentItem?.estimate?.estimate;
if (priority && estimate) {
return (
<p>
Du er nummer {priority} i køen ({getWaitingPeriodText(estimate)}).
</p>
);
}
};
}) {
// Return success message if reservation completed
if (completed && !error) {
const { mainTitle, mediaType, sellingPoint, imageObj } = publication;
......@@ -74,21 +43,17 @@ const ReservationForm = ({
</div>
<div className="reservation-success-form__card-text">
<Block top={0}>
{renderQueueInformation()}
{renderQueueInformation(reservationWork)}
<p>Du vil beskjed når bestillingen er klar for henting.</p>
<br />
<p>Husk at du alltid kan se og administrere</p>
<p>
bestillingene dine {" "}
<p className="my_page-info">
Husk at du alltid kan se og administrere bestillingene dine {" "}
<a className="link" onClick={onRedirectClose}>
Min side.
</a>
</p>
</Block>
<Block top={4}>
<Hr />
</Block>
<Hr />
<Block className="reservation-success-form__card-button" top={4}>
<Button full primary onClick={onClose}>
......@@ -101,17 +66,7 @@ const ReservationForm = ({
);
}
// TEMP: Remove Hovedbiblioteket from available pick-up branches
let branches = allBranches
.filter(b => !b.tempClosedForPickup)
.filter(b => b.value !== "hutl");
if (limitedToBranches.length > 0) {
branches = branches.filter(branch =>
limitedToBranches.includes(branch.value)
);
}
const sortedBranches = sortBy(branches, ["text"]);
const sortedBranches = sortBy(allBranches, ["text"]);
return (
<form onSubmit={onSubmit}>
......@@ -167,18 +122,16 @@ const ReservationForm = ({
</Block>
</form>
);
};
}
ReservationForm.defaultProps = {
error: false,
recordId: "",
limitedToBranches: []
};
ReservationForm.propTypes = {
isLoggedIn: PropTypes.bool.isRequired,
allBranches: PropTypes.array.isRequired,
recordId: PropTypes.string,
selectedBranchId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool])
.isRequired,
onChangeBranch: PropTypes.func.isRequired,
......@@ -189,19 +142,21 @@ ReservationForm.propTypes = {
error: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
limitedToBranches: PropTypes.array,
reservationWork: PropTypes.object.isRequired,
workData: PropTypes.object.isRequired,
onRedirectClose: PropTypes.func.isRequired
};
const mapStateToProps = state => {
const { work } = state.resources;
// ----------- Utils below -------------
return {
workData: work.currentItem
};
};
function renderQueueInformation(reservationWork) {
const currentItem = reservationWork?.currentItem;
const priority = currentItem?.pri;
const estimate = currentItem?.estimate?.estimate;
export default connect(
mapStateToProps,
{}
)(ReservationForm);
if (priority && estimate) {
return (
<p>
Du er nummer {priority} i køen ({getWaitingPeriodText(estimate)}).
</p>
);
}
}
.reservation-success-form__card {
margin-top: var(--spacing-6);
margin-top: var(--spacing-6);
}
/* On screens that are 650px or more, set display to horizontal */
@media screen and (min-width: 650px) {
.reservation-success-form {
margin-bottom: var(--spacing-4);
}
.reservation-success-form {
margin-bottom: var(--spacing-4);
}
.reservation-success-form__card {
display: flex;
}
.reservation-success-form__card {
display: flex;
}
.reservation-success-form__card-text {
padding-left: var(--spacing-6);
position: relative;
.reservation-success-form__card-text {
padding-left: var(--spacing-6);
display: flex;
flex-direction: column;
& > * {
margin-top: var(--spacing-4);
&:first-child {
margin-top: 0;
flex-grow: 1;
}
}
.reservation-success-form__card-button {
position: absolute;
bottom: 0;
left:0;
right:0;
padding-left: var(--spacing-6);
padding-bottom: var(--spacing-1);
& .my_page-info {
margin-top: 1.5rem;
}
}
}
@media screen and (max-width: 649px) {
.reservation-success-form__card-text {
margin-top: var(--spacing-6);
}
}
\ No newline at end of file
.reservation-success-form__card-text {
margin-top: var(--spacing-6);
}
}
import React, { Fragment } from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
import autoBind from "auto-bind";
import { connect } from "react-redux";
......@@ -22,9 +21,8 @@ import RemoteReservationForm from "../RemoteReservationForm";
import { Modal } from "@digibib/deichman-ui";
import "./styles.css";
import ReservationSelector from "./ReservationSelector";
import { setBodyFreeze } from "../../utilities/accessibility";
import ModalPortal from "../ModalPortal/ModalPortal";
let modalRoot;
/**
* Responsible for reserving items from the catalog
* -> Shows loginform if user is not logged in
......@@ -50,19 +48,16 @@ class ReservationContainer extends React.Component {
nextProps.homeBranch &&
nextProps.branches.length > 0
) {
// Check if homeBranch is valud
// Check if homeBranch is valid
const homeBranchIsValid = nextProps.branches.some(
b =>
b.value === nextProps.homeBranch &&
!b.tempClosedForPickup &&
b.value !== "hutl"
b => b.value === nextProps.homeBranch && !b.tempClosedForPickup
);
// default to Hovedbiblioteket if homebranch
// default to Bjørvika if homebranch
// is missing, unknown or closed for pickups
const selectedBranchId = homeBranchIsValid
? nextProps.homeBranch
: "fmaj";
: "bjor";
return {
selectedBranchId
......@@ -73,11 +68,8 @@ class ReservationContainer extends React.Component {
}
componentDidMount() {
setBodyFreeze(true);
this.props.fetchKohaBranches();
setTimeout(() => this.setState({ show: true }), 100);
modalRoot = document.getElementById("modalRoot");
modalRoot.appendChild(this.el);
}
componentDidUpdate() {
......@@ -87,11 +79,6 @@ class ReservationContainer extends React.Component {
}
}
componentWillUnmount() {
setBodyFreeze(false);
modalRoot && modalRoot.removeChild(this.el);
}
handleChangeBranch(e) {
this.setState({
selectedBranchId: e.target.value
......@@ -197,76 +184,81 @@ class ReservationContainer extends React.Component {
limitedToBranches = items.map(item => item.branchcode);
}
return ReactDOM.createPortal(
<div className="reservation-wrapper">
<FocusTrap
active={this.state.show}
focusTrapOptions={{
initialFocus: "#reservation-trap"
}}
>
<Modal
name="Reserver"
visible={this.state.show}
onClose={this.handleCloseModal(false)}
sizeW="40rem"
showClose
const availableBranches =
limitedToBranches.length > 0
? branches.filter(branch => limitedToBranches.includes(branch.value))
: branches;
return (
<ModalPortal>
<div className="reservation-wrapper">
<FocusTrap
active={this.state.show}
focusTrapOptions={{
initialFocus: "#reservation-trap"
}}
>
<div
className="focus-trap-target reservation-content"
id="reservation-trap"
tabIndex="-1"
<Modal
name="Reserver"
visible={this.state.show}
onClose={this.handleCloseModal(false)}
sizeW="40rem"
showClose
>
<Fragment>
{!publication && (
<ReservationSelector
onClick={this.setPublication}
copies={copies}
publications={publications}
currentPublication={this.props.currentPublication}
/>
)}
{publication && (
<>
{isRemoteReservation ? (
<RemoteReservationForm
borrowerId={borrowerId}
onChangeBorrowerId={this.handleChangeBorrowerId}
onClose={this.handleCloseModal(false)}
isSubmitting={reservationInProgress}
isLoggedIn={isLoggedIn}
completed={reservationCompleted}
error={reservationError}
recordId={recordId}
numHolds={`${Number(numHolds)}`}
publicationTitle={title}
onSubmit={this.handleSubmit}
/>
) : (
<ReservationForm
allBranches={branches}
selectedBranchId={selectedBranchId}
onChangeBranch={this.handleChangeBranch}
onClose={this.handleCloseModal(false)}
isSubmitting={reservationInProgress}
isLoggedIn={isLoggedIn}
completed={reservationCompleted}
error={reservationError}
publication={publication}
limitedToBranches={limitedToBranches}
onSubmit={this.handleSubmit}
reservationWork={work}
onRedirectClose={this.handleCloseModal(true)}
/>
)}
</>
)}
</Fragment>
</div>
</Modal>
</FocusTrap>
</div>,
this.el
<div
className="focus-trap-target reservation-content"
id="reservation-trap"
tabIndex="-1"
>
<Fragment>
{!publication && (
<ReservationSelector
onClick={this.setPublication}
copies={copies}
publications={publications}
currentPublication={this.props.currentPublication}
/>
)}
{publication && (
<>
{isRemoteReservation ? (
<RemoteReservationForm
borrowerId={borrowerId}
onChangeBorrowerId={this.handleChangeBorrowerId}
onClose={this.handleCloseModal(false)}
isSubmitting={reservationInProgress}
isLoggedIn={isLoggedIn}
completed={reservationCompleted}
error={reservationError}
recordId={recordId}
numHolds={`${Number(numHolds)}`}
publicationTitle={title}
onSubmit={this.handleSubmit}
/>
) : (
<ReservationForm
allBranches={availableBranches}
selectedBranchId={selectedBranchId}
onChangeBranch={this.handleChangeBranch}
onClose={this.handleCloseModal(false)}
isSubmitting={reservationInProgress}
isLoggedIn={isLoggedIn}
completed={reservationCompleted}
error={reservationError}
publication={publication}
limitedToBranches={limitedToBranches}
onSubmit={this.handleSubmit}
reservationWork={work}
onRedirectClose={this.handleCloseModal(true)}
/>
)}
</>
)}
</Fragment>
</div>
</Modal>
</FocusTrap>
</div>
</ModalPortal>
);
}
}
......
......@@ -37,6 +37,8 @@ import { matchMedia } from "../../utilities/media";
import { uniqBy } from "lodash";
import ReservationContainer from "../ReservationsContainer/ReservationContainer";
import { useEffect } from "react";
import { useSelector } from "react-redux";
import LoginButton from "../LoginButton/LoginButton";
export default function ReservePublicationWidget({
publications,
......@@ -46,9 +48,11 @@ export default function ReservePublicationWidget({
const [language, setLanguage] = useState(languages?.[0]);
const [format, setFormat] = useState(formats?.[0]);
const [platform, setPlatform] = useState(platforms?.[0]);
const [copies, setCopies] = useState({});
const [reservationPublications, setReservationPublications] = useState([]);
const isLoggedIn = useSelector(state => state.auth.isLoggedIn);
const mediaType = currentPublication.mediaType;
const title = matchMedia(titles, mediaType);
const showFields = matchMedia(availableFields, mediaType);
......@@ -137,29 +141,38 @@ export default function ReservePublicationWidget({
return (
<div className="reserve-work-widget">
<h3>{title}</h3>
<div>
{reserveChoices.map(choice => {