|
1 | | -import PropTypes from 'prop-types'; |
2 | | -import React from 'react'; |
| 1 | +import React, { useRef, useState } from 'react'; |
3 | 2 | import classNames from 'classnames'; |
4 | | -import { withTranslation } from 'react-i18next'; |
| 3 | +import { useTranslation } from 'react-i18next'; |
| 4 | +import { useDispatch, useSelector } from 'react-redux'; |
| 5 | +import { |
| 6 | + closeProjectOptions, |
| 7 | + newFile, |
| 8 | + newFolder, |
| 9 | + openProjectOptions, |
| 10 | + openUploadFileModal |
| 11 | +} from '../actions/ide'; |
| 12 | +import { getAuthenticated, selectCanEditSketch } from '../selectors/users'; |
5 | 13 |
|
6 | 14 | import ConnectedFileNode from './FileNode'; |
7 | 15 |
|
8 | 16 | import DownArrowIcon from '../../../images/down-filled-triangle.svg'; |
9 | 17 |
|
10 | | -class Sidebar extends React.Component { |
11 | | - constructor(props) { |
12 | | - super(props); |
13 | | - this.resetSelectedFile = this.resetSelectedFile.bind(this); |
14 | | - this.toggleProjectOptions = this.toggleProjectOptions.bind(this); |
15 | | - this.onBlurComponent = this.onBlurComponent.bind(this); |
16 | | - this.onFocusComponent = this.onFocusComponent.bind(this); |
| 18 | +// TODO: use a generic Dropdown UI component |
17 | 19 |
|
18 | | - this.state = { |
19 | | - isFocused: false |
20 | | - }; |
21 | | - } |
| 20 | +export default function SideBar() { |
| 21 | + const { t } = useTranslation(); |
| 22 | + const dispatch = useDispatch(); |
22 | 23 |
|
23 | | - onBlurComponent() { |
24 | | - this.setState({ isFocused: false }); |
| 24 | + const [isFocused, setIsFocused] = useState(false); |
| 25 | + |
| 26 | + const files = useSelector((state) => state.files); |
| 27 | + // TODO: use `selectRootFile` defined in another PR |
| 28 | + const rootFile = files.filter((file) => file.name === 'root')[0]; |
| 29 | + const projectOptionsVisible = useSelector( |
| 30 | + (state) => state.ide.projectOptionsVisible |
| 31 | + ); |
| 32 | + const isExpanded = useSelector((state) => state.ide.sidebarIsExpanded); |
| 33 | + const canEditProject = useSelector(selectCanEditSketch); |
| 34 | + const isAuthenticated = useSelector(getAuthenticated); |
| 35 | + |
| 36 | + const sidebarOptionsRef = useRef(null); |
| 37 | + |
| 38 | + const onBlurComponent = () => { |
| 39 | + setIsFocused(false); |
25 | 40 | setTimeout(() => { |
26 | | - if (!this.state.isFocused) { |
27 | | - this.props.closeProjectOptions(); |
| 41 | + if (!isFocused) { |
| 42 | + dispatch(closeProjectOptions()); |
28 | 43 | } |
29 | 44 | }, 200); |
30 | | - } |
| 45 | + }; |
31 | 46 |
|
32 | | - onFocusComponent() { |
33 | | - this.setState({ isFocused: true }); |
34 | | - } |
| 47 | + const onFocusComponent = () => { |
| 48 | + setIsFocused(true); |
| 49 | + }; |
35 | 50 |
|
36 | | - resetSelectedFile() { |
37 | | - this.props.setSelectedFile(this.props.files[1].id); |
38 | | - } |
39 | | - |
40 | | - toggleProjectOptions(e) { |
| 51 | + const toggleProjectOptions = (e) => { |
41 | 52 | e.preventDefault(); |
42 | | - if (this.props.projectOptionsVisible) { |
43 | | - this.props.closeProjectOptions(); |
| 53 | + if (projectOptionsVisible) { |
| 54 | + dispatch(closeProjectOptions()); |
44 | 55 | } else { |
45 | | - this.sidebarOptions.focus(); |
46 | | - this.props.openProjectOptions(); |
| 56 | + sidebarOptionsRef.current?.focus(); |
| 57 | + dispatch(openProjectOptions()); |
47 | 58 | } |
48 | | - } |
| 59 | + }; |
49 | 60 |
|
50 | | - userCanEditProject() { |
51 | | - let canEdit; |
52 | | - if (!this.props.owner) { |
53 | | - canEdit = true; |
54 | | - } else if ( |
55 | | - this.props.user.authenticated && |
56 | | - this.props.owner.id === this.props.user.id |
57 | | - ) { |
58 | | - canEdit = true; |
59 | | - } else { |
60 | | - canEdit = false; |
61 | | - } |
62 | | - return canEdit; |
63 | | - } |
64 | | - |
65 | | - render() { |
66 | | - const canEditProject = this.userCanEditProject(); |
67 | | - const sidebarClass = classNames({ |
68 | | - sidebar: true, |
69 | | - 'sidebar--contracted': !this.props.isExpanded, |
70 | | - 'sidebar--project-options': this.props.projectOptionsVisible, |
71 | | - 'sidebar--cant-edit': !canEditProject |
72 | | - }); |
73 | | - const rootFile = this.props.files.filter((file) => file.name === 'root')[0]; |
| 61 | + const sidebarClass = classNames({ |
| 62 | + sidebar: true, |
| 63 | + 'sidebar--contracted': !isExpanded, |
| 64 | + 'sidebar--project-options': projectOptionsVisible, |
| 65 | + 'sidebar--cant-edit': !canEditProject |
| 66 | + }); |
74 | 67 |
|
75 | | - return ( |
76 | | - <section className={sidebarClass}> |
77 | | - <header |
78 | | - className="sidebar__header" |
79 | | - onContextMenu={this.toggleProjectOptions} |
80 | | - > |
81 | | - <h3 className="sidebar__title"> |
82 | | - <span>{this.props.t('Sidebar.Title')}</span> |
83 | | - </h3> |
84 | | - <div className="sidebar__icons"> |
85 | | - <button |
86 | | - aria-label={this.props.t('Sidebar.ToggleARIA')} |
87 | | - className="sidebar__add" |
88 | | - tabIndex="0" |
89 | | - ref={(element) => { |
90 | | - this.sidebarOptions = element; |
91 | | - }} |
92 | | - onClick={this.toggleProjectOptions} |
93 | | - onBlur={this.onBlurComponent} |
94 | | - onFocus={this.onFocusComponent} |
95 | | - > |
96 | | - <DownArrowIcon focusable="false" aria-hidden="true" /> |
97 | | - </button> |
98 | | - <ul className="sidebar__project-options"> |
| 68 | + return ( |
| 69 | + <section className={sidebarClass}> |
| 70 | + <header className="sidebar__header" onContextMenu={toggleProjectOptions}> |
| 71 | + <h3 className="sidebar__title"> |
| 72 | + <span>{t('Sidebar.Title')}</span> |
| 73 | + </h3> |
| 74 | + <div className="sidebar__icons"> |
| 75 | + <button |
| 76 | + aria-label={t('Sidebar.ToggleARIA')} |
| 77 | + className="sidebar__add" |
| 78 | + tabIndex="0" |
| 79 | + ref={sidebarOptionsRef} |
| 80 | + onClick={toggleProjectOptions} |
| 81 | + onBlur={onBlurComponent} |
| 82 | + onFocus={onFocusComponent} |
| 83 | + > |
| 84 | + <DownArrowIcon focusable="false" aria-hidden="true" /> |
| 85 | + </button> |
| 86 | + <ul className="sidebar__project-options"> |
| 87 | + <li> |
| 88 | + <button |
| 89 | + aria-label={t('Sidebar.AddFolderARIA')} |
| 90 | + onClick={() => { |
| 91 | + dispatch(newFolder(rootFile.id)); |
| 92 | + setTimeout(() => dispatch(closeProjectOptions()), 0); |
| 93 | + }} |
| 94 | + onBlur={onBlurComponent} |
| 95 | + onFocus={onFocusComponent} |
| 96 | + > |
| 97 | + {t('Sidebar.AddFolder')} |
| 98 | + </button> |
| 99 | + </li> |
| 100 | + <li> |
| 101 | + <button |
| 102 | + aria-label={t('Sidebar.AddFileARIA')} |
| 103 | + onClick={() => { |
| 104 | + dispatch(newFile(rootFile.id)); |
| 105 | + setTimeout(() => dispatch(closeProjectOptions()), 0); |
| 106 | + }} |
| 107 | + onBlur={onBlurComponent} |
| 108 | + onFocus={onFocusComponent} |
| 109 | + > |
| 110 | + {t('Sidebar.AddFile')} |
| 111 | + </button> |
| 112 | + </li> |
| 113 | + {isAuthenticated && ( |
99 | 114 | <li> |
100 | 115 | <button |
101 | | - aria-label={this.props.t('Sidebar.AddFolderARIA')} |
| 116 | + aria-label={t('Sidebar.UploadFileARIA')} |
102 | 117 | onClick={() => { |
103 | | - this.props.newFolder(rootFile.id); |
104 | | - setTimeout(this.props.closeProjectOptions, 0); |
| 118 | + dispatch(openUploadFileModal(rootFile.id)); |
| 119 | + setTimeout(() => dispatch(closeProjectOptions()), 0); |
105 | 120 | }} |
106 | | - onBlur={this.onBlurComponent} |
107 | | - onFocus={this.onFocusComponent} |
| 121 | + onBlur={onBlurComponent} |
| 122 | + onFocus={onFocusComponent} |
108 | 123 | > |
109 | | - {this.props.t('Sidebar.AddFolder')} |
| 124 | + {t('Sidebar.UploadFile')} |
110 | 125 | </button> |
111 | 126 | </li> |
112 | | - <li> |
113 | | - <button |
114 | | - aria-label={this.props.t('Sidebar.AddFileARIA')} |
115 | | - onClick={() => { |
116 | | - this.props.newFile(rootFile.id); |
117 | | - setTimeout(this.props.closeProjectOptions, 0); |
118 | | - }} |
119 | | - onBlur={this.onBlurComponent} |
120 | | - onFocus={this.onFocusComponent} |
121 | | - > |
122 | | - {this.props.t('Sidebar.AddFile')} |
123 | | - </button> |
124 | | - </li> |
125 | | - {this.props.user.authenticated && ( |
126 | | - <li> |
127 | | - <button |
128 | | - aria-label={this.props.t('Sidebar.UploadFileARIA')} |
129 | | - onClick={() => { |
130 | | - this.props.openUploadFileModal(rootFile.id); |
131 | | - setTimeout(this.props.closeProjectOptions, 0); |
132 | | - }} |
133 | | - onBlur={this.onBlurComponent} |
134 | | - onFocus={this.onFocusComponent} |
135 | | - > |
136 | | - {this.props.t('Sidebar.UploadFile')} |
137 | | - </button> |
138 | | - </li> |
139 | | - )} |
140 | | - </ul> |
141 | | - </div> |
142 | | - </header> |
143 | | - <ConnectedFileNode id={rootFile.id} canEdit={canEditProject} /> |
144 | | - </section> |
145 | | - ); |
146 | | - } |
| 127 | + )} |
| 128 | + </ul> |
| 129 | + </div> |
| 130 | + </header> |
| 131 | + <ConnectedFileNode id={rootFile.id} canEdit={canEditProject} /> |
| 132 | + </section> |
| 133 | + ); |
147 | 134 | } |
148 | | - |
149 | | -Sidebar.propTypes = { |
150 | | - files: PropTypes.arrayOf( |
151 | | - PropTypes.shape({ |
152 | | - name: PropTypes.string.isRequired, |
153 | | - id: PropTypes.string.isRequired |
154 | | - }) |
155 | | - ).isRequired, |
156 | | - setSelectedFile: PropTypes.func.isRequired, |
157 | | - isExpanded: PropTypes.bool.isRequired, |
158 | | - projectOptionsVisible: PropTypes.bool.isRequired, |
159 | | - newFile: PropTypes.func.isRequired, |
160 | | - openProjectOptions: PropTypes.func.isRequired, |
161 | | - closeProjectOptions: PropTypes.func.isRequired, |
162 | | - newFolder: PropTypes.func.isRequired, |
163 | | - openUploadFileModal: PropTypes.func.isRequired, |
164 | | - owner: PropTypes.shape({ |
165 | | - id: PropTypes.string |
166 | | - }), |
167 | | - user: PropTypes.shape({ |
168 | | - id: PropTypes.string, |
169 | | - authenticated: PropTypes.bool.isRequired |
170 | | - }).isRequired, |
171 | | - t: PropTypes.func.isRequired |
172 | | -}; |
173 | | - |
174 | | -Sidebar.defaultProps = { |
175 | | - owner: undefined |
176 | | -}; |
177 | | - |
178 | | -export default withTranslation()(Sidebar); |
0 commit comments