|
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