From 5adfc09237cb0eead02a8b2a9cf8fb2f2332a5fc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 18 Jan 2019 13:43:17 -0700 Subject: [PATCH] Bring in TabbedView nearly verbatim from prior work Sourced from https://github.com/matrix-org/matrix-react-sdk/pull/1644 and related PRs. --- res/css/_components.scss | 1 + res/css/structures/_TabbedView.scss | 76 ++++++++ res/themes/dharma/css/_dharma.scss | 8 + res/themes/light/css/_base.scss | 9 + src/components/structures/MatrixChat.js | 4 +- src/components/structures/TabbedView.js | 165 ++++++++++++++++++ .../views/dialogs/UserSettingsDialog.js | 45 +++-- src/i18n/strings/en_EN.json | 4 +- 8 files changed, 284 insertions(+), 28 deletions(-) create mode 100644 res/css/structures/_TabbedView.scss create mode 100644 src/components/structures/TabbedView.js diff --git a/res/css/_components.scss b/res/css/_components.scss index a1b575a0a1..1b35332711 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -18,6 +18,7 @@ @import "./structures/_RoomSubList.scss"; @import "./structures/_RoomView.scss"; @import "./structures/_SearchBox.scss"; +@import "./structures/_TabbedView.scss"; @import "./structures/_TagPanel.scss"; @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss new file mode 100644 index 0000000000..7d42823d17 --- /dev/null +++ b/res/css/structures/_TabbedView.scss @@ -0,0 +1,76 @@ +/* +Copyright 2017 Travis Ralston + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_TabbedView { + margin: 0; + padding: 0; + display: flex; + width: 100%; + height: 100%; + background-color: $tab-panel-bg-color; +} + +.mx_TabbedView_tabLabels { + width: 300px; + height: 100%; + background-color: $tab-list-bg-color; + color: $tab-list-fg-color; + border-right: 1px solid $tab-border-color; + border-left: 1px solid $tab-border-color; +} + +.mx_TabbedView_tabPanels { + width: calc(100% - 320px); + display: inline-block; + height: 100%; + padding-left: 20px; + scroll-snap-type: block; +} + +.mx_TabbedView_tabLabel { + text-align: center; + vertical-align: middle; + text-transform: uppercase; + cursor: pointer; + display: block; + padding: 20px; + width: calc(100% - 40px); + border-bottom: 1px solid $tab-border-color; +} + +.mx_TabbedView_exit { + padding-top: 10px; + padding-bottom: 10px; +} + +.mx_TabbedView_tabLabel:hover { + font-weight: 700; +} + +.mx_TabbedView_tabLabel_active { + font-weight: 700; + background-color: $tab-list-active-bg-color; + color: $tab-list-active-fg-color; +} + +.mx_TabbedView_tabPanel { + height: 100vh; // 100% of viewport height + scroll-snap-align: start; +} + +.mx_TabbedView_tabPanelContent { + width: 600px; +} \ No newline at end of file diff --git a/res/themes/dharma/css/_dharma.scss b/res/themes/dharma/css/_dharma.scss index c9f62fbe6b..8f369a3b33 100644 --- a/res/themes/dharma/css/_dharma.scss +++ b/res/themes/dharma/css/_dharma.scss @@ -186,6 +186,14 @@ $lightbox-bg-color: #454545; $lightbox-fg-color: #ffffff; $lightbox-border-color: #ffffff; +// Tabbed views +$tab-list-bg-color: $secondary-accent-color; +$tab-list-fg-color: $accent-color; +$tab-list-active-bg-color: $tertiary-accent-color; +$tab-list-active-fg-color: $accent-color; +$tab-border-color: $tertiary-accent-color; +$tab-panel-bg-color: #fff; + // unused? $progressbar-color: #000; diff --git a/res/themes/light/css/_base.scss b/res/themes/light/css/_base.scss index dbab909f13..996fe965cf 100644 --- a/res/themes/light/css/_base.scss +++ b/res/themes/light/css/_base.scss @@ -66,6 +66,7 @@ $primary-hairline-color: #e5e5e5; // used for the border of input text fields $input-border-color: #f0f0f0; +$input-border-dark-color: #b8b8b8; $input-darker-bg-color: #c1c9d6; $input-darker-fg-color: #9fa9ba; @@ -181,6 +182,14 @@ $imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); $imagebody-giflabel-color: rgba(255, 255, 255, 1); +// Tabbed views +$tab-list-bg-color: $secondary-accent-color; +$tab-list-fg-color: $accent-color; +$tab-list-active-bg-color: $tertiary-accent-color; +$tab-list-active-fg-color: $accent-color; +$tab-border-color: $tertiary-accent-color; +$tab-panel-bg-color: #fff; + // unused? $progressbar-color: #000; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 25ba980bcf..9733576bd0 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -612,9 +612,7 @@ export default React.createClass({ break; case 'view_user_settings': const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); - Modal.createTrackedDialog('User settings', '', UserSettingsDialog, { - title: _t("Settings"), - }); + Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}); //this._setPage(PageTypes.UserSettings); //this.notifyNewScreen('settings'); break; diff --git a/src/components/structures/TabbedView.js b/src/components/structures/TabbedView.js new file mode 100644 index 0000000000..44ecee7a95 --- /dev/null +++ b/src/components/structures/TabbedView.js @@ -0,0 +1,165 @@ +/* +Copyright 2017 Travis Ralston +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as React from "react"; +import {_t, _td} from '../../languageHandler'; +import GeminiScrollbar from 'react-gemini-scrollbar'; +import PropTypes from "prop-types"; +//import scrollSnapPolyfill from 'css-scroll-snap-polyfill'; + +const DEFAULT_EXIT_STRING = _td("Return to app"); + +/** + * Represents a tab for the TabbedView + */ +export class Tab { + /** + * Creates a new tab + * @param {string} tabLabel The untranslated tab label + * @param {string} tabJsx The JSX for the tab container. + */ + constructor(tabLabel, tabJsx) { + this.label = tabLabel; + this.body = tabJsx; + } +} + +export class TabbedView extends React.Component { + constructor() { + super(); + + // This is used to track when the user has scrolled all the way up or down so we + // don't immediately start flipping between tabs. + this._reachedEndAt = 0; + } + + getInitialState() { + return { + activeTabIndex: 0, + }; + } + + _getActiveTabIndex() { + return this.state ? this.state.activeTabIndex : 0; + } + + /** + * Shows the given tab + * @param {Tab} tab the tab to show + * @private + */ + _setActiveTab(tab) { + const idx = this.props.tabs.indexOf(tab); + if (idx !== -1) { + this.setState({activeTabIndex: idx}); + this._reachedEndAt = 0; // reset scroll timer + } + else console.error("Could not find tab " + tab.label + " in tabs"); + } + + _nextTab() { + let targetIndex = this._getActiveTabIndex() + 1; + if (targetIndex < this.props.tabs.length) { + this.setState({activeTabIndex: targetIndex}); + this._reachedEndAt = 0; // reset scroll timer + } + } + + _previousTab() { + let targetIndex = this._getActiveTabIndex() - 1; + if (targetIndex >= 0) { + this.setState({activeTabIndex: targetIndex}); + this._reachedEndAt = 0; // reset scroll timer + } + } + + _getTabLabel(tab) { + let classes = "mx_TabbedView_tabLabel "; + + const idx = this.props.tabs.indexOf(tab); + if (idx === this._getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active"; + + return ( + this._setActiveTab(tab)}> + {_t(tab.label)} + + ); + } + + _getTabPanel(tab) { + return ( +
+ {tab.body} +
+ ); + } + + componentDidUpdate() { + window.requestAnimationFrame(() => { + console.log("SCROLL SNAP POLYFILL: UPDATE"); + //scrollSnapPolyfill(); + }); + } + + componentDidMount() { + window.requestAnimationFrame(() => { + console.log("SCROLL SNAP POLYFILL: MOUNT"); + //scrollSnapPolyfill(); + }); + } + + render() { + const labels = []; + const tabs = []; + + for (const tab of this.props.tabs) { + labels.push(this._getTabLabel(tab)); + tabs.push(this._getTabPanel(tab)); + } + + const returnToApp = ( + + {_t(this.props.exitLabel || DEFAULT_EXIT_STRING)} + + ); + + return ( +
+
+ {returnToApp} + {labels} +
+
+ {tabs} +
+
+ ); + } +} + +TabbedView.PropTypes = { + // Called when the user clicks the "Exit" or "Return to app" button + onExit: PropTypes.func.isRequired, + + // The untranslated label for the "Return to app" button. + // Default: "Return to app" + exitLabel: PropTypes.string, + + // The tabs to show + tabs: PropTypes.arrayOf(Tab).isRequired, +}; \ No newline at end of file diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index d12895010c..f04b92b6cc 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -17,33 +17,30 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import {_t} from '../../../languageHandler'; -import SdkConfig from "../../../SdkConfig"; +import {Tab, TabbedView} from "../../structures/TabbedView"; +import {_td} from "../../../languageHandler"; -export default React.createClass({ - propTypes: { +export default class UserSettingsDialog extends React.Component { + static propTypes = { onFinished: PropTypes.func.isRequired, - }, + }; - render: function () { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const UserSettings = sdk.getComponent('structures.UserSettings'); + _getTabs() { + return [ + new Tab(_td("General"),
General Test
), + new Tab(_td("Account"),
Account Test
), + ]; + } + render() { return ( - -
- -
-
+ + // ); - }, -}); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cfd5168152..cc18731ee0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1043,6 +1043,8 @@ "Room contains unknown devices": "Room contains unknown devices", "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", "Unknown devices": "Unknown devices", + "General": "General", + "Account": "Account", "Unable to load backup status": "Unable to load backup status", "Unable to restore backup": "Unable to restore backup", "No backup found!": "No backup found!", @@ -1222,6 +1224,7 @@ "Click to unmute audio": "Click to unmute audio", "Click to mute audio": "Click to mute audio", "Filter room names": "Filter room names", + "Return to app": "Return to app", "Clear filter": "Clear filter", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", @@ -1286,7 +1289,6 @@ "Add email address": "Add email address", "Profile": "Profile", "Display name": "Display name", - "Account": "Account", "To return to your account in future you need to set a password": "To return to your account in future you need to set a password", "Logged in as:": "Logged in as:", "Access Token:": "Access Token:",