Skip to content

Commit

Permalink
console,account: Convert class components to functional components
Browse files Browse the repository at this point in the history
  • Loading branch information
kschiffer committed Sep 29, 2023
1 parent 1daa35a commit 73312ea
Show file tree
Hide file tree
Showing 4 changed files with 474 additions and 568 deletions.
344 changes: 152 additions & 192 deletions pkg/webui/components/navigation/side/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@
// limitations under the License.

import ReactDom from 'react-dom'
import React, { Component } from 'react'
import bind from 'autobind-decorator'
import React, { useState, useEffect, useCallback, useRef } from 'react'
import classnames from 'classnames'
import { defineMessages, injectIntl } from 'react-intl'
import { defineMessages, useIntl } from 'react-intl'

import LAYOUT from '@ttn-lw/constants/layout'

Expand All @@ -41,225 +40,186 @@ const m = defineMessages({
hideSidebar: 'Hide sidebar',
})

@injectIntl
export class SideNavigation extends Component {
static propTypes = {
appContainerId: PropTypes.string,
children: PropTypes.node.isRequired,
className: PropTypes.string,
/** The header for the side navigation. */
header: PropTypes.shape({
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
iconAlt: PropTypes.message.isRequired,
to: PropTypes.string.isRequired,
}).isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func,
}).isRequired,
modifyAppContainerClasses: PropTypes.bool,
}

static defaultProps = {
appContainerId: 'app',
modifyAppContainerClasses: true,
className: undefined,
}

state = {
/** A flag specifying whether the side navigation is minimized or not. */
isMinimized: getViewportWidth() <= LAYOUT.BREAKPOINTS.M,
/** A flag specifying whether the drawer is currently open (in mobile
* screensizes).
*/
isDrawerOpen: false,
/** A flag indicating whether the user has last toggled the sidebar to
* minimized state. */
preferMinimized: false,
}

@bind
updateAppContainerClasses(initial = false) {
const { modifyAppContainerClasses, appContainerId } = this.props
if (!modifyAppContainerClasses) {
return
}
const { isMinimized } = this.state
const containerClasses = document.getElementById(appContainerId).classList
containerClasses.add('with-sidebar')
if (!initial) {
// The transitioned class is necessary to prevent unwanted width
// transitions during route changes.
containerClasses.add('sidebar-transitioned')
}
if (isMinimized) {
containerClasses.add('sidebar-minimized')
} else {
containerClasses.remove('sidebar-minimized')
}
}

@bind
removeAppContainerClasses() {
const { modifyAppContainerClasses, appContainerId } = this.props
const SideNavigation = ({
appContainerId,
modifyAppContainerClasses,
className,
header,
children,
}) => {
const [isMinimized, setIsMinimized] = useState(getViewportWidth() <= LAYOUT.BREAKPOINTS.M)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [preferMinimized, setPreferMinimized] = useState(false)
const node = useRef()
const intl = useIntl()

const updateAppContainerClasses = useCallback(
(initial = false) => {
if (!modifyAppContainerClasses) {
return
}
const containerClasses = document.getElementById(appContainerId).classList
containerClasses.add('with-sidebar')
if (!initial) {
containerClasses.add('sidebar-transitioned')
}
if (isMinimized) {
containerClasses.add('sidebar-minimized')
} else {
containerClasses.remove('sidebar-minimized')
}
},
[modifyAppContainerClasses, appContainerId, isMinimized],
)

const removeAppContainerClasses = useCallback(() => {
if (!modifyAppContainerClasses) {
return
}
document
.getElementById(appContainerId)
.classList.remove('with-sidebar', 'sidebar-minimized', 'sidebar-transitioned')
}
}, [modifyAppContainerClasses, appContainerId])

componentDidMount() {
window.addEventListener('resize', this.setMinimizedState)
this.updateAppContainerClasses(true)
}
const closeDrawer = useCallback(() => {
setIsDrawerOpen(false)
document.body.classList.remove(style.scrollLock)
}, [])

componentWillUnmount() {
window.removeEventListener('resize', this.setMinimizedState)
this.removeAppContainerClasses()
}
const openDrawer = useCallback(() => {
setIsDrawerOpen(true)
document.body.classList.add(style.scrollLock)
}, [])

@bind
setMinimizedState() {
const { isMinimized, preferMinimized } = this.state
useEffect(() => {
const onClickOutside = e => {
if (isDrawerOpen && node.current && !node.current.contains(e.target)) {
closeDrawer()
}
}

if (isDrawerOpen) {
document.addEventListener('mousedown', onClickOutside)
return () => document.removeEventListener('mousedown', onClickOutside)
}
}, [isDrawerOpen, closeDrawer])

const setMinimizedState = useCallback(() => {
const viewportWidth = getViewportWidth()
if (
(!isMinimized && viewportWidth <= LAYOUT.BREAKPOINTS.M) ||
(isMinimized && viewportWidth > LAYOUT.BREAKPOINTS.M)
) {
this.setState({ isMinimized: getViewportWidth() <= LAYOUT.BREAKPOINTS.M || preferMinimized })
this.updateAppContainerClasses()
setIsMinimized(getViewportWidth() <= LAYOUT.BREAKPOINTS.M || preferMinimized)
updateAppContainerClasses()
}
}

@bind
async onToggle() {
await this.setState(prev => ({
isMinimized: !prev.isMinimized,
preferMinimized: !prev.isMinimized,
}))
this.updateAppContainerClasses()
}
}, [isMinimized, preferMinimized, updateAppContainerClasses])

useEffect(() => {
window.addEventListener('resize', setMinimizedState)
updateAppContainerClasses(true)
return () => {
window.removeEventListener('resize', setMinimizedState)
removeAppContainerClasses()
}
}, [removeAppContainerClasses, setMinimizedState, updateAppContainerClasses])

@bind
onDrawerExpandClick() {
const { isDrawerOpen } = this.state
const onToggle = useCallback(async () => {
setIsMinimized(prev => !prev)
setPreferMinimized(prev => !prev)
updateAppContainerClasses()
}, [updateAppContainerClasses])

const onDrawerExpandClick = useCallback(() => {
if (!isDrawerOpen) {
this.openDrawer()
openDrawer()
} else {
this.closeDrawer()
closeDrawer()
}
}
}, [isDrawerOpen, openDrawer, closeDrawer])

@bind
onClickOutside(e) {
const { isDrawerOpen } = this.state
if (isDrawerOpen && this.node && !this.node.contains(e.target)) {
this.closeDrawer()
}
}

@bind
closeDrawer() {
this.setState({ isDrawerOpen: false })

// Enable body scrolling.
document.body.classList.remove(style.scrollLock)
document.removeEventListener('mousedown', this.onClickOutside)
}

@bind
openDrawer() {
// Disable body scrolling.
document.body.classList.add(style.scrollLock)

document.addEventListener('mousedown', this.onClickOutside)
this.setState({ isDrawerOpen: true })
}

@bind
onLeafItemClick() {
const { isDrawerOpen } = this.state
const onLeafItemClick = useCallback(() => {
if (isDrawerOpen) {
this.onDrawerExpandClick()
onDrawerExpandClick()
}
}

@bind
ref(node) {
this.node = node
}

render() {
const { className, header, children, intl } = this.props
const { isMinimized, isDrawerOpen } = this.state

const navigationClassNames = classnames(className, style.navigation, {
[style.navigationMinimized]: isMinimized,
})
const minimizeButtonClassNames = classnames(style.minimizeButton, {
[style.minimizeButtonMinimized]: isMinimized,
})

const drawerClassNames = classnames(style.drawer, { [style.drawerOpen]: isDrawerOpen })

return (
<>
<nav className={navigationClassNames} ref={this.ref} data-test-id="navigation-sidebar">
<div className={style.mobileHeader} onClick={this.onDrawerExpandClick}>
<Icon className={style.expandIcon} icon="more_vert" />
<img
className={style.icon}
src={header.icon}
alt={intl.formatMessage(header.iconAlt)}
/>
<Message className={style.message} content={header.title} />
</div>
<div>
<div className={drawerClassNames}>
<Link to={header.to}>
<div className={style.header}>
<img
className={style.icon}
src={header.icon}
alt={intl.formatMessage(header.iconAlt)}
/>
<Message className={style.message} content={header.title} />
</div>
</Link>
<SideNavigationContext.Provider
value={{ isMinimized, onLeafItemClick: this.onLeafItemClick }}
}, [isDrawerOpen, onDrawerExpandClick])

const navigationClassNames = classnames(className, style.navigation, {
[style.navigationMinimized]: isMinimized,
})
const minimizeButtonClassNames = classnames(style.minimizeButton, {
[style.minimizeButtonMinimized]: isMinimized,
})

const drawerClassNames = classnames(style.drawer, { [style.drawerOpen]: isDrawerOpen })

return (
<>
<nav className={navigationClassNames} ref={node} data-test-id="navigation-sidebar">
<div className={style.mobileHeader} onClick={onDrawerExpandClick}>
<Icon className={style.expandIcon} icon="more_vert" />
<img className={style.icon} src={header.icon} alt={intl.formatMessage(header.iconAlt)} />
<Message className={style.message} content={header.title} />
</div>
<div>
<div className={drawerClassNames}>
<Link to={header.to}>
<div className={style.header}>
<img
className={style.icon}
src={header.icon}
alt={intl.formatMessage(header.iconAlt)}
/>
<Message className={style.message} content={header.title} />
</div>
</Link>
<SideNavigationContext.Provider value={{ isMinimized, onLeafItemClick }}>
<SideNavigationList
onListClick={onDrawerExpandClick}
isMinimized={isMinimized}
className={style.navigationList}
>
<SideNavigationList
onListClick={this.onDrawerExpandClick}
isMinimized={isMinimized}
className={style.navigationList}
>
{children}
</SideNavigationList>
</SideNavigationContext.Provider>
</div>
{children}
</SideNavigationList>
</SideNavigationContext.Provider>
</div>
</nav>
<Button
unstyled
className={minimizeButtonClassNames}
icon={isMinimized ? 'keyboard_arrow_right' : 'keyboard_arrow_left'}
message={isMinimized ? null : m.hideSidebar}
onClick={this.onToggle}
data-hook="side-nav-hide-button"
/>
</>
)
}
</div>
</nav>
<Button
unstyled
className={minimizeButtonClassNames}
icon={isMinimized ? 'keyboard_arrow_right' : 'keyboard_arrow_left'}
message={isMinimized ? null : m.hideSidebar}
onClick={onToggle}
data-hook="side-nav-hide-button"
/>
</>
)
}

SideNavigation.propTypes = {
appContainerId: PropTypes.string,
children: PropTypes.node.isRequired,
className: PropTypes.string,
/** The header for the side navigation. */
header: PropTypes.shape({
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
iconAlt: PropTypes.message.isRequired,
to: PropTypes.string.isRequired,
}).isRequired,
modifyAppContainerClasses: PropTypes.bool,
}

SideNavigation.defaultProps = {
appContainerId: 'app',
modifyAppContainerClasses: true,
className: undefined,
}

const PortalledSideNavigation = props =>
ReactDom.createPortal(<SideNavigation {...props} />, document.getElementById('sidebar'))

PortalledSideNavigation.Item = SideNavigationItem

export default PortalledSideNavigation
export { PortalledSideNavigation as default, SideNavigation }
Loading

0 comments on commit 73312ea

Please sign in to comment.