Verified Commit cec75a6e authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Improve animation handling of FilteredListPage

parent b403be16
Pipeline #58020 failed with stages
in 9 minutes and 42 seconds
......@@ -4,8 +4,14 @@ import { Link as MaterialLink } from '@material-ui/core'
import { Link as GatsbyPluginIntlLink } from 'gatsby-plugin-intl'
const Link = ({ children, ...props }) => {
// eslint-disable-next-line no-unused-vars
const MyGatsbyPluginIntlLink = React.forwardRef((nestedProps, ref) => {
return <GatsbyPluginIntlLink {...nestedProps} />
})
MyGatsbyPluginIntlLink.displayName = 'MyGatsbyPluginIntlLink'
return (
<MaterialLink {...props} component={GatsbyPluginIntlLink}>
<MaterialLink {...props} component={MyGatsbyPluginIntlLink}>
{children}
</MaterialLink>
)
......
......@@ -3,7 +3,6 @@ import marked from 'marked'
import escape from 'html-escape'
import PropTypes from 'prop-types'
import { useIntl } from 'gatsby-plugin-intl'
import { Typography } from '@material-ui/core'
import { Parser as HtmlToReactParser } from 'html-to-react'
import TranslatedAlert from './translatedAlert'
......@@ -13,7 +12,6 @@ const TranslatedContent = ({
parseMarkdown,
noEscape,
noHint,
typographyProps,
...props
}) => {
const intl = useIntl()
......@@ -44,18 +42,14 @@ const TranslatedContent = ({
return (
<div {...props}>
{!noHint && hint}
<Typography {...typographyProps}>
{htmlToReactParser.parse(
parseMarkdown && message ? marked(message) : message
)}
</Typography>
{htmlToReactParser.parse(
parseMarkdown && message ? marked(message) : message
)}
</div>
)
}
TranslatedContent.propTypes = {
/** Additional properties for the typography child component */
typographyProps: PropTypes.object,
/** Content for all available languages */
content: PropTypes.object.isRequired,
/** Specifies to parse markdown */
......@@ -74,8 +68,4 @@ TranslatedContent.propTypes = {
noHint: PropTypes.bool,
}
TranslatedContent.defaultProps = {
typographyProps: {},
}
export default TranslatedContent
......@@ -14,7 +14,6 @@ const StudydocumentFormCourseYearField = ({ value, onChange, ...props }) => {
type="number"
onChange={e => {
const { value: newValue } = e.target
console.log(newValue)
onChange({
name: 'course_year',
value: newValue,
......
import { useState, useEffect, useRef } from 'react'
import { useDispatch } from 'react-redux'
import animateScrollTo from 'animated-scroll-to'
import { useIntl } from 'gatsby-plugin-intl'
import { useTheme } from '@material-ui/core'
import { loadItem } from '../store/common/actions'
const PATH_UPDATE_DELAY = 250 // in milliseconds
/**
* useFilteredList hook - common logic for FilteredListPages
*
* Handles the path changes when an item is expanded/collapsed so that
* the animation is visible and the page does not scroll unpredictably.
*
* Also handles pinned items and whether it should scroll to the
* active item.
*
* @param {string} resource API resource name
* @param {string} pathPrefix prefix of the resource path
* (between language code and item id)
* @param {array} lists array of lists
* @param {object} items dictionary of items accessible by their id.
* @param {string} itemId id of the item originally requested by the page.
* @param {bool} itemsReady if `true`, ready to check if requested item is loaded.
*/
const useFilteredList = (
resource,
pathPrefix,
lists,
items,
itemId,
itemsReady = true
) => {
const dispatch = useDispatch()
// Set to the selected item id on initial page load.
const [storedItemId, setStoredItemId] = useState(null)
const [navigateTimeout, setNavigateTimeout] = useState(null)
const [pinnedId, setPinnedId] = useState(null)
const [shouldScroll, setShouldScroll] = useState(!!itemId)
const theme = useTheme()
const ref = useRef(null)
const intl = useIntl()
useEffect(() => {
setStoredItemId(itemId)
setShouldScroll(true)
}, [itemId])
// Handle set of pinned item on initial load.
useEffect(() => {
if (!storedItemId || !itemsReady) return
// Load item if not already loaded.
if (!items[storedItemId]) {
dispatch(loadItem(resource, { id: storedItemId }))
}
// Check if item already part of a list. If not, make it pinned at the top of the page.
if (lists.every(list => !list.items.includes(storedItemId))) {
setPinnedId(storedItemId)
}
}, [storedItemId, itemsReady])
// Handle initial scroll to active element
useEffect(() => {
if (itemsReady && ref.current && shouldScroll) {
setShouldScroll(false)
animateScrollTo(ref.current, {
verticalOffset: -theme.shape.headerHeight,
speed: 125,
})
}
}, [itemsReady, ref.current, shouldScroll])
const itemExpandHandler = ({ id, expanded }) => {
let path = null
if (expanded) {
path = `/${intl.locale}/${pathPrefix}/${id}`
setStoredItemId(id)
} else {
path = `/${intl.locale}/${pathPrefix}`
setStoredItemId(null)
}
// The timeout is needed so that the animation is performed before switching
// the page. Otherwise the CSS animations will not trigger.
if (navigateTimeout) {
clearTimeout(navigateTimeout)
}
setNavigateTimeout(
setTimeout(() => {
if (window.location.pathname !== path) {
// ATTENTION!
// This assumes that the same component is used for list and detail view!
// If this is not the case, use `navigate` from `gatsby-plugin-intl` instead.
// eslint-disable-next-line no-restricted-globals
history.pushState(null, '', path)
}
}, PATH_UPDATE_DELAY)
)
}
return [ref, pinnedId, storedItemId, itemExpandHandler]
}
export default useFilteredList
import React, { useEffect, useState, useRef } from 'react'
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import { FormattedMessage, navigate } from 'gatsby-plugin-intl'
import animateScrollTo from 'animated-scroll-to'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import { FormattedMessage } from 'gatsby-plugin-intl'
import { makeStyles } from '@material-ui/core/styles'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import {
Typography,
......@@ -12,13 +11,10 @@ import {
ExpansionPanelSummary,
} from '@material-ui/core'
import {
loadItem,
listLoadNextPage,
listLoadAllPages,
} from '~store/common/actions'
import { listLoadNextPage, listLoadAllPages } from '~store/common/actions'
import { EVENTS } from '~store/events/constants'
import useFilteredList from '../../hooks/useFilteredList'
import Layout from '../../components/layout'
import EventsFilter from '../../components/events/filter'
import FilteredListLayout from '../../components/filteredListPage/layout'
......@@ -33,11 +29,14 @@ const useStyles = makeStyles(
textAlign: 'center',
paddingTop: '2em',
},
calendarRoot: {
backgroundColor: theme.palette.common.grey,
calendarPanel: {
marginBottom: '2em',
overflow: 'hidden',
borderRadius: '4px',
},
calendarSummary: {
backgroundColor: theme.palette.common.grey,
},
calendarDetails: {
padding: 0,
},
......@@ -45,7 +44,7 @@ const useStyles = makeStyles(
{ name: 'events' }
)
const EventsPage = ({ eventId }) => {
const EventsPage = ({ eventId: eventIdProp }) => {
// Selectors for all event lists
const upcomingWithOpenRegistration = useSelector(
state => state.events.upcomingWithOpenRegistration
......@@ -58,12 +57,19 @@ const EventsPage = ({ eventId }) => {
const events = useSelector(state => state.events.items)
const dispatch = useDispatch()
// Set to the selected event id on initial page load.
const [pinnedId, setPinnedId] = useState(null)
const [shouldScroll, setShouldScroll] = useState(!!eventId)
const itemsReady =
upcomingWithOpenRegistration.totalPages > 0 &&
upcomingWithoutOpenRegistration.totalPages > 0 &&
past.totalPages > 0
const [ref, pinnedId, eventId, itemExpandHandler] = useFilteredList(
EVENTS,
'events',
[upcomingWithOpenRegistration, upcomingWithoutOpenRegistration, past],
events,
eventIdProp,
itemsReady
)
const classes = useStyles()
const theme = useTheme()
const ref = useRef(null)
useEffect(() => {
if (
......@@ -95,38 +101,6 @@ const EventsPage = ({ eventId }) => {
}
}, [past])
// Handle set of pinned item on initial load.
useEffect(() => {
if (!eventId) return
// Load event if not already loaded.
if (!events[eventId]) {
dispatch(loadItem(EVENTS, { id: eventId }))
}
// Check if item already part of a list. If not, make it pinned at the top of the page.
if (
!(
upcomingWithOpenRegistration.items.includes(eventId) ||
upcomingWithoutOpenRegistration.items.includes(eventId) ||
past.items.includes(eventId)
)
) {
setPinnedId(eventId)
}
}, [eventId])
// Handle initial scroll to active element
useEffect(() => {
if (ref.current && shouldScroll) {
setShouldScroll(false)
animateScrollTo(ref.current, {
verticalOffset: -theme.shape.headerHeight,
speed: 125,
})
}
}, [ref.current, shouldScroll])
// Helper functions for shown lists and list items
const isActive = item => item === eventId
const hasMorePagesToLoad = list => list.lastPageLoaded < list.totalPages
......@@ -135,19 +109,6 @@ const EventsPage = ({ eventId }) => {
dispatch(listLoadNextPage(EVENTS, { listName: list }))
}
const eventExpandHandler = ({ id, expanded }) => {
let path = null
if (expanded) {
path = `/events/${id}`
} else {
path = `/events`
}
if (window.location.pathname !== path) {
navigate(path)
}
}
const placeholder = (
<FilteredListItem disabled>
<EventSummary eventId={null} />
......@@ -160,10 +121,10 @@ const EventsPage = ({ eventId }) => {
<EventsFilter />
{/* Event Calendar */}
<ExpansionPanel>
<ExpansionPanel className={classes.calendarPanel}>
<ExpansionPanelSummary
classes={{
root: classes.calendarRoot,
root: classes.calendarSummary,
}}
expandIcon={<ExpandMoreIcon />}
>
......@@ -189,7 +150,7 @@ const EventsPage = ({ eventId }) => {
id={pinnedId}
ref={isActive(pinnedId) ? ref : undefined}
expanded={isActive(pinnedId)}
onChange={eventExpandHandler}
onChange={itemExpandHandler}
>
<EventSummary eventId={pinnedId} />
<EventDetails eventId={pinnedId} />
......@@ -215,7 +176,7 @@ const EventsPage = ({ eventId }) => {
id={item}
ref={isActive(item) ? ref : undefined}
expanded={isActive(item)}
onChange={eventExpandHandler}
onChange={itemExpandHandler}
>
<EventSummary eventId={item} />
<EventDetails eventId={item} />
......@@ -241,7 +202,7 @@ const EventsPage = ({ eventId }) => {
id={item}
ref={isActive(item) ? ref : undefined}
expanded={isActive(item)}
onChange={eventExpandHandler}
onChange={itemExpandHandler}
>
<EventSummary eventId={item} />
<EventDetails eventId={item} />
......@@ -263,7 +224,7 @@ const EventsPage = ({ eventId }) => {
id={item}
ref={isActive(item) ? ref : undefined}
expanded={isActive(item)}
onChange={eventExpandHandler}
onChange={itemExpandHandler}
>
<EventSummary eventId={item} />
<EventDetails eventId={item} />
......
import React, { useEffect, useState, useRef } from 'react'
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import { navigate } from 'gatsby-plugin-intl'
import animateScrollTo from 'animated-scroll-to'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import { makeStyles } from '@material-ui/core/styles'
import {
loadItem,
listLoadNextPage,
listLoadAllPages,
} from '~store/common/actions'
import { listLoadNextPage, listLoadAllPages } from '~store/common/actions'
import { JOBOFFERS } from '../../store/joboffers/constants'
import Layout from '../../components/layout'
......@@ -19,6 +13,7 @@ import FilteredList from '../../components/filteredListPage/list'
import FilteredListItem from '../../components/filteredListPage/listItem'
import JobofferSummary from '../../components/jobs/summary'
import JobofferDetails from '../../components/jobs/details'
import useFilteredList from '../../hooks/useFilteredList'
const useStyles = makeStyles(
{
......@@ -30,19 +25,23 @@ const useStyles = makeStyles(
{ name: 'jobs' }
)
const JobsPage = ({ jobofferId }) => {
// Selectors for all event lists
const JobsPage = ({ jobofferId: jobofferIdProp }) => {
// Selectors for all joboffer lists
const defaultList = useSelector(state => state.joboffers.default)
// Selector for selected item (loaded from path)
const joboffers = useSelector(state => state.joboffers.items)
const dispatch = useDispatch()
// Set to the selected event id on initial page load.
const [pinnedId, setPinnedId] = useState(null)
const [shouldScroll, setShouldScroll] = useState(!!jobofferId)
const itemsReady = defaultList.totalPages > 0
const [ref, pinnedId, jobofferId, itemExpandHandler] = useFilteredList(
JOBOFFERS,
'jobs',
[defaultList],
joboffers,
jobofferIdProp,
itemsReady
)
const classes = useStyles()
const theme = useTheme()
const ref = useRef(null)
useEffect(() => {
if (defaultList.totalPages === 0 && !defaultList.isPending) {
......@@ -50,32 +49,6 @@ const JobsPage = ({ jobofferId }) => {
}
}, [defaultList])
// Handle set of pinned item on initial load.
useEffect(() => {
if (!jobofferId) return
// Load event if not already loaded.
if (!joboffers[jobofferId]) {
dispatch(loadItem(JOBOFFERS, { id: jobofferId }))
}
// Check if item already part of a list. If not, make it pinned at the top of the page.
if (!defaultList.items.includes(jobofferId)) {
setPinnedId(jobofferId)
}
}, [jobofferId])
// Handle initial scroll to active element
useEffect(() => {
if (ref.current && shouldScroll) {
setShouldScroll(false)
animateScrollTo(ref.current, {
verticalOffset: -theme.shape.headerHeight,
speed: 125,
})
}
}, [ref.current, shouldScroll])
// Helper functions for shown lists and list items
const isActive = item => item === jobofferId
const hasMorePagesToLoad = list => list.lastPageLoaded < list.totalPages
......@@ -84,19 +57,6 @@ const JobsPage = ({ jobofferId }) => {
dispatch(listLoadNextPage(JOBOFFERS, { listName: list }))
}
const itemExpandHandler = ({ id, expanded }) => {
let path = null
if (expanded) {
path = `/jobs/${id}`
} else {
path = `/jobs`
}
if (window.location.pathname !== path) {
navigate(path)
}
}
const placeholder = (
<React.Fragment>
{Array.from(Array(3)).map((_, i) => (
......
import React, { useEffect, useState, useRef } from 'react'
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import { navigate, FormattedMessage } from 'gatsby-plugin-intl'
import animateScrollTo from 'animated-scroll-to'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import { FormattedMessage } from 'gatsby-plugin-intl'
import { makeStyles } from '@material-ui/core/styles'
import { loadItem, listLoadNextPage } from '~store/common/actions'
import { listLoadNextPage } from '~store/common/actions'
import { STUDYDOCUMENTS } from '../../store/studydocuments/constants'
import Layout from '../../components/layout'
......@@ -18,6 +17,7 @@ import StudydocumentDetails from '../../components/studydocuments/details'
import StudydocumentsQuickFilter from '../../components/studydocuments/quickFilter'
import StudydocumentsOralExamsDialog from '../../components/studydocuments/oralExamsDialog'
import StudydocumentsUploadHint from '../../components/studydocuments/documentUploadHint'
import useFilteredList from '../../hooks/useFilteredList'
const useStyles = makeStyles(
{
......@@ -29,20 +29,24 @@ const useStyles = makeStyles(
{ name: 'studydocuments' }
)
const StudydocumentsPage = ({ studydocumentId }) => {
// Selectors for all event lists
const StudydocumentsPage = ({ studydocumentId: studydocumentIdProp }) => {
// Selectors for all studydocuments lists
const defaultList = useSelector(state => state.studydocuments.default)
// Selector for selected item (loaded from path)
const studydocuments = useSelector(state => state.studydocuments.items)
const auth = useSelector(state => state.auth)
const dispatch = useDispatch()
// Set to the selected event id on initial page load.
const [pinnedId, setPinnedId] = useState(null)
const [shouldScroll, setShouldScroll] = useState(!!studydocumentId)
const itemsReady = auth.isLoggedIn && defaultList.totalPages > 0
const [ref, pinnedId, studydocumentId, itemExpandHandler] = useFilteredList(
STUDYDOCUMENTS,
'studydocuments',
[defaultList],
studydocuments,
studydocumentIdProp,
itemsReady
)
const classes = useStyles()
const theme = useTheme()
const ref = useRef(null)
useEffect(() => {
if (
......@@ -55,30 +59,30 @@ const StudydocumentsPage = ({ studydocumentId }) => {
}, [defaultList, auth.isLoggedIn])
// Handle set of pinned item on initial load.
useEffect(() => {
if (!studydocumentId || !auth.isLoggedIn) return
// useEffect(() => {
// if (!studydocumentId || !auth.isLoggedIn) return
// Load studydocument if not already loaded.
if (!studydocuments[studydocumentId]) {
dispatch(loadItem(STUDYDOCUMENTS, { id: studydocumentId }))
}
// // Load studydocument if not already loaded.
// if (!studydocuments[studydocumentId]) {
// dispatch(loadItem(STUDYDOCUMENTS, { id: studydocumentId }))
// }
// Check if item already part of a list. If not, make it pinned at the top of the page.
if (!defaultList.items.includes(studydocumentId)) {
setPinnedId(studydocumentId)
}
}, [studydocumentId, auth.isLoggedIn])
// // Check if item already part of a list. If not, make it pinned at the top of the page.
// if (!defaultList.items.includes(studydocumentId)) {
// setPinnedId(studydocumentId)
// }
// }, [studydocumentId, auth.isLoggedIn])
// Handle initial scroll to active element
useEffect(() => {
if (ref.current && shouldScroll) {
setShouldScroll(false)
animateScrollTo(ref.current, {
verticalOffset: -theme.shape.headerHeight,
speed: 125,
})
}
}, [ref.current, shouldScroll])
// useEffect(() => {
// if (ref.current && shouldScroll) {
// setShouldScroll(false)
// animateScrollTo(ref.current, {
// verticalOffset: -theme.shape.headerHeight,
// speed: 125,
// })
// }
// }, [ref.current, shouldScroll])
// Helper functions for shown lists and list items
const isActive = item => item === studydocumentId
......@@ -88,18 +92,18 @@ const StudydocumentsPage = ({ studydocumentId }) => {
dispatch(listLoadNextPage(STUDYDOCUMENTS, { listName: list }))
}
const itemExpandHandler = ({ id, expanded }) => {
let path = null
if (expanded) {
path = `/studydocuments/${id}`
} else {
path = `/studydocuments`
}
if (window.location.pathname !== path) {
navigate(path)
}
}
// const itemExpandHandler = ({ id, expanded }) => {
// let path = null
// if (expanded) {
// path = `/studydocuments/${id}`
// } else {
// path = `/studydocuments`
// }
// if (window.location.pathname !== path) {
// navigate(path)
// }
// }
const placeholder = (
<React.Fragment>
......