From c14ad16d549f35d19054d1539cad738ba06a642f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CCezar=20Augusto=E2=80=9C?= Date: Sat, 28 Jan 2017 00:20:23 -0200 Subject: [PATCH] Responsive tab / refactor to Aphrodite Auditors: @bsclifton, @bbondy /cc @bradleyrichter Fix #5431 Fix #6511 --- app/renderer/components/styles/global.js | 27 +- app/renderer/components/styles/tab.js | 126 ++++++++ app/renderer/components/tabContent.js | 316 +++++++++++++++++++ app/renderer/components/tabIcon.js | 47 --- app/renderer/lib/tabUtil.js | 24 ++ docs/state.md | 4 +- docs/windowActions.md | 24 ++ js/actions/windowActions.js | 28 ++ js/components/tab.js | 209 +++++++------ js/components/tabsToolbar.js | 1 + js/constants/windowConstants.js | 2 + js/lib/throttle.js | 20 ++ js/stores/windowStore.js | 8 + less/tabs.less | 128 -------- test/unit/app/renderer/tabContentTest.js | 380 +++++++++++++++++++++++ test/unit/app/renderer/tabIconTest.js | 50 --- 16 files changed, 1072 insertions(+), 322 deletions(-) create mode 100644 app/renderer/components/styles/tab.js create mode 100644 app/renderer/components/tabContent.js delete mode 100644 app/renderer/components/tabIcon.js create mode 100644 app/renderer/lib/tabUtil.js create mode 100644 js/lib/throttle.js create mode 100644 test/unit/app/renderer/tabContentTest.js delete mode 100644 test/unit/app/renderer/tabIconTest.js diff --git a/app/renderer/components/styles/global.js b/app/renderer/components/styles/global.js index 18d7d62d524..193eb29d1b3 100644 --- a/app/renderer/components/styles/global.js +++ b/app/renderer/components/styles/global.js @@ -4,7 +4,15 @@ const globalStyles = { breakpointNarrowViewport: '600px', breakpointExtensionButtonPadding: '720px', breakpointSmallWin32: '650px', - breakpointTinyWin32: '500px' + breakpointTinyWin32: '500px', + tab: { + largeMedium: '83px', + medium: '66px', + mediumSmall: '53px', + small: '42px', + extraSmall: '33px', + smallest: '19px' + } }, color: { linkColor: '#0099CC', @@ -92,7 +100,9 @@ const globalStyles = { navbarBraveButtonMarginLeft: '80px', navbarLeftMarginDarwin: '76px', sideBarWidth: '190px', - aboutPageSectionPadding: '24px' + aboutPageSectionPadding: '24px', + defaultTabPadding: '0 4px', + defaultIconPadding: '0 2px' }, shadow: { switchShadow: 'inset 0 1px 4px rgba(0, 0, 0, 0.35)', @@ -133,6 +143,19 @@ const globalStyles = { zindexSuggestionText: '3100', zindexWindowFullScreen: '4000', zindexWindowFullScreenBanner: '4100' + }, + fontSize: { + tabIcon: '14px', + tabTitle: '12px' + }, + appIcons: { + loading: 'fa fa-spinner fa-spin', + defaultIcon: 'fa fa-file-o', + closeTab: 'fa fa-times-circle', + private: 'fa fa-eye', + newSession: 'fa fa-user', + volumeOn: 'fa fa-volume-up', + volumeOff: 'fa fa-volume-off' } } diff --git a/app/renderer/components/styles/tab.js b/app/renderer/components/styles/tab.js new file mode 100644 index 00000000000..c01c601442f --- /dev/null +++ b/app/renderer/components/styles/tab.js @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const {StyleSheet} = require('aphrodite') +const globalStyles = require('./global') + +const styles = StyleSheet.create({ + // Windows specific style + tabForWindows: { + color: '#555' + }, + + tab: { + background: 'linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1))', + borderRadius: `${globalStyles.radius.borderRadiusTabs} ${globalStyles.radius.borderRadiusTabs} 0 0`, + borderWidth: '1px 1px 0', + borderStyle: 'solid', + borderColor: 'transparent', + boxSizing: 'border-box', + color: '#3B3B3B', + display: 'flex', + height: '23px', + marginTop: '2px', + transition: 'transform 200ms ease', + left: '0', + opacity: '1', + width: '100%', + alignItems: 'center', + justifyContent: 'space-between', + padding: globalStyles.spacing.defaultTabPadding, + position: 'relative', + + ':hover': { + background: 'linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(250, 250, 250, 0.4))' + } + }, + + // Custom classes based on tab's width and behaviour + + tabNarrowView: { + padding: '0 2px' + }, + + narrowViewPlayIndicator: { + borderWidth: '2px 0 0', + borderStyle: 'solid', + borderColor: 'lightskyblue' + }, + + tabNarrowestView: { + justifyContent: 'center' + }, + + tabMinAllowedSize: { + padding: 0 + }, + + tabIdNarrowView: { + flex: 'inherit' + }, + + tabIdMinAllowedSize: { + overflow: 'hidden' + }, + + // Add extra space for pages that have no icon + // such as about:blank and about:newtab + noFavicon: { + padding: '0 6px' + }, + + alternativePlayIndicator: { + borderTop: '2px solid lightskyblue' + }, + + tabId: { + alignItems: 'center', + display: 'flex', + flex: '1', + minWidth: '0' // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1108514#c5 + }, + + isPinned: { + padding: globalStyles.spacing.defaultIconPadding + }, + + active: { + background: `linear-gradient(to bottom, #fff, ${globalStyles.color.chromePrimary})`, + height: '25px', + marginTop: '1px', + boxShadow: '0 -1px 4px 0 rgba(51, 51, 51, 0.12)', + borderWidth: '1px 1px 0', + borderStyle: 'solid', + borderColor: '#bbb', + + ':hover': { + background: `linear-gradient(to bottom, #fff, ${globalStyles.color.chromePrimary})` + } + }, + + activePrivateTab: { + background: 'rgb(247, 247, 247)', + color: 'black' + }, + + private: { + background: '#9c8dc1', // (globalStyles.color.privateTabBackground, 40%) + color: '#fff', + + ':hover': { + background: '#665296', // (globalStyles.color.privateTabBackground, 20%) + color: '#fff' + } + }, + + dragging: { + ':hover': { + closeTab: { + opacity: '0' + } + } + } +}) + +module.exports = styles diff --git a/app/renderer/components/tabContent.js b/app/renderer/components/tabContent.js new file mode 100644 index 00000000000..07ac3140918 --- /dev/null +++ b/app/renderer/components/tabContent.js @@ -0,0 +1,316 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const React = require('react') +const ImmutableComponent = require('../../../js/components/immutableComponent') +const {StyleSheet, css} = require('aphrodite/no-important') +const globalStyles = require('./styles/global') + +/** + * Boilerplate component for all tab icons + */ +class TabIcon extends ImmutableComponent { + render () { + const tabIconStyle = { + // Currently it's not possible to concatenate Aphrodite generated classes + // and pre-built classes using default Aphrodite API, so we keep with inline-style + fontSize: 'inherit', + display: 'flex', + alignSelf: 'center', + width: '16px', + height: '16px', + alignItems: 'center', + justifyContent: 'center' + } + return
+ { + this.props.symbol + ? + : null + } +
+ } +} + +class Favicon extends ImmutableComponent { + get tabProps () { + return this.props.tabProps + } + + get favicon () { + return !this.props.isLoading && this.tabProps.get('icon') + } + + get loadingIcon () { + return this.props.isLoading + ? globalStyles.appIcons.loading + : null + } + + get isPageWithoutFavicon () { + return this.tabProps.get('location') === 'about:newtab' + } + + get defaultIcon () { + return (!this.props.isLoading && !this.favicon) + ? globalStyles.appIcons.defaultIcon + : null + } + + get faviconCSSClass () { + const iconStyles = StyleSheet.create({ + favicon: {backgroundImage: `url(${this.favicon})`} + }) + return css( + styles.icon, + this.favicon && iconStyles.favicon + ) + } + + get faviconNarrowView () { + return !this.props.isPinned && this.tabProps.get('breakpoint') === 'extraSmall' && styles.iconNarrowView + } + + render () { + return !this.isPageWithoutFavicon + ? + : null + } +} + +class AudioTabIcon extends ImmutableComponent { + get tabProps () { + return this.props.tabProps + } + + get pageCanPlayAudio () { + return this.tabProps.get('audioPlaybackActive') || this.tabProps.get('audioMuted') + } + + get narrowView () { + const sizes = ['medium', 'mediumSmall', 'small', 'extraSmall', 'smallest'] + return sizes.includes(this.tabProps.get('breakpoint')) + } + + get locationHasSecondaryIcon () { + return !!this.tabProps.get('isPrivate') || !!this.tabProps.get('partitionNumber') + } + + get matchingCriteria () { + return this.pageCanPlayAudio && !this.narrowView + } + + get mutedState () { + return this.pageCanPlayAudio && this.tabProps.get('audioMuted') + } + + get unmutedState () { + this.tabProps.get('audioPlaybackActive') && !this.tabProps.get('audioMuted') + } + + get audioIcon () { + return !this.mutedState + ? globalStyles.appIcons.volumeOn + : globalStyles.appIcons.volumeOff + } + + get audioIconCSSClass () { + return css(styles.icon, styles.audioIcon) + } + + render () { + return this.matchingCriteria + ? + : null + } +} + +class PrivateIcon extends ImmutableComponent { + get tabProps () { + return this.props.tabProps + } + + get matchingCriteria () { + const sizes = ['small', 'extraSmall', 'smallest'] + return this.tabProps.get('isPrivate') && + !sizes.includes(this.tabProps.get('breakpoint')) && + !this.tabProps.get('hoverState') + } + + render () { + return this.matchingCriteria + ? + : null + } +} + +class NewSessionIcon extends ImmutableComponent { + get tabProps () { + return this.props.tabProps + } + + get matchingCriteria () { + const sizes = ['small', 'extraSmall', 'smallest'] + return this.tabProps.get('partitionNumber') && + !sizes.includes(this.tabProps.get('breakpoint')) && + !this.tabProps.get('hoverState') + } + + render () { + return this.matchingCriteria + ? + : null + } +} + +class TabTitle extends ImmutableComponent { + get tabProps () { + return this.props.tabProps + } + get locationHasSecondaryIcon () { + return !!this.tabProps.get('isPrivate') || !!this.tabProps.get('partitionNumber') + } + + get isPinned () { + return !!this.tabProps.get('pinnedLocation') + } + + get pageCanPlayAudio () { + return this.tabProps.get('audioPlaybackActive') || this.tabProps.get('audioMuted') + } + + get shouldHideTitle () { + return ( + this.tabProps.get('breakpoint') === 'largeMedium' && + this.pageCanPlayAudio && this.locationHasSecondaryIcon + ) || + (this.tabProps.get('breakpoint') === 'mediumSmall' && this.locationHasSecondaryIcon) || + this.tabProps.get('breakpoint') === 'extraSmall' || this.tabProps.get('breakpoint') === 'smallest' + } + + get titleCSSClass () { + return css( + styles.tabTitle, + // Windows specific style + process.platform === 'win32' && styles.tabTitleForWindows, + // Linux specific style + process.platform === 'linux' && styles.tabTitleForLinux + ) + } + + render () { + return !this.isPinned && !this.shouldHideTitle + ?
{this.props.pageTitle}
+ : null + } +} + +class CloseTabIcon extends ImmutableComponent { + get tabProps () { + return this.props.tabProps + } + + get isPinned () { + return !!this.tabProps.get('pinnedLocation') + } + + get matchingCriteria () { + const sizes = ['extraSmall', 'smallest'] + return this.tabProps.get('hoverState') && + !sizes.includes(this.tabProps.get('breakpoint')) && + !this.isPinned + } + + render () { + return this.matchingCriteria + ? + : null + } +} + +const styles = StyleSheet.create({ + icon: { + width: '16px', + minWidth: '16px', + height: '16px', + backgroundSize: '16px', + fontSize: globalStyles.fontSize.tabIcon, + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + display: 'flex', + alignSelf: 'center', + position: 'relative', + textAlign: 'center', + justifyContent: 'center', + padding: globalStyles.spacing.defaultIconPadding + }, + + iconNarrowView: { + padding: 0 + }, + + audioIcon: { + color: globalStyles.color.highlightBlue + }, + + closeTab: { + opacity: '0.7', + // background: 'linear-gradient(to left, rgba(255,255,255, 1) 20%, rgba(255,255,255, 0) 80%)', + position: 'absolute', + top: '0', + right: '0', + padding: '0 4px', + borderTopRightRadius: globalStyles.radius.borderRadius, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '16px', + width: '16px', + height: '100%', + border: '0', + zIndex: globalStyles.zindex.zindexTabs, + + ':hover': { + opacity: '1' + } + }, + + tabTitle: { + WebkitUserSelect: 'none', + boxSizing: 'border-box', + fontSize: globalStyles.fontSize.tabTitle, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + height: '15px', + padding: globalStyles.spacing.defaultTabPadding + }, + + tabTitleForWindows: { + fontWeight: '500', + fontSize: globalStyles.fontSize.tabTitle, + height: '18px' + }, + + tabTitleForLinux: { + height: globalStyles.fontSize.tabTitle + } +}) + +module.exports = { + Favicon, + AudioTabIcon, + NewSessionIcon, + PrivateIcon, + TabTitle, + CloseTabIcon +} diff --git a/app/renderer/components/tabIcon.js b/app/renderer/components/tabIcon.js deleted file mode 100644 index 00d6b5174d7..00000000000 --- a/app/renderer/components/tabIcon.js +++ /dev/null @@ -1,47 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const React = require('react') -const ImmutableComponent = require('../../../js/components/immutableComponent') -const {StyleSheet, css} = require('aphrodite') -const globalStyles = require('./styles/global') - -class TabIcon extends ImmutableComponent { - render () { - const className = css( - styles.icon, - this.props.withBlueIcon && styles.blueIcon - ) - return
- -
- } -} - -class AudioTabIcon extends ImmutableComponent { - render () { - return - } -} - -const styles = StyleSheet.create({ - 'icon': { - backgroundPosition: 'center', - backgroundRepeat: 'no-repeat', - display: 'inline-block', - fontSize: '14px', - margin: 'auto 7px auto 7px', - position: 'relative', - verticalAlign: 'middle', - textAlign: 'center' - }, - 'blueIcon': { - color: globalStyles.color.highlightBlue - } -}) - -module.exports = { - TabIcon, - AudioTabIcon -} diff --git a/app/renderer/lib/tabUtil.js b/app/renderer/lib/tabUtil.js new file mode 100644 index 00000000000..64d1a2ffe75 --- /dev/null +++ b/app/renderer/lib/tabUtil.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const styles = require('../components/styles/global') + +/** + * Get tab's breakpoint name for current tab size. + * @param {String} The current tab size + * @returns {String} The matching breakpoint. + */ +module.exports.getTabBreakpoint = (tabWidth) => { + const sizes = ['largeMedium', 'medium', 'mediumSmall', 'small', 'extraSmall', 'smallest'] + let currentSize + + sizes.map(size => { + if (tabWidth <= Number.parseInt(styles.breakpoint.tab[size], 10)) { + currentSize = size + return false + } + return true + }) + return currentSize +} diff --git a/docs/state.md b/docs/state.md index 1a0d9a0d6f3..5f781c12e00 100644 --- a/docs/state.md +++ b/docs/state.md @@ -308,7 +308,9 @@ WindowStore icon: string, // favicon url location: string, // The currently navigated location loading: boolean, - frameKey: number + frameKey: number, + breakpoint: string, // breakpoint name for current tab size, specified in app/renderer/components/styles/tab.js + hoverState: boolean // current tab hover state }], frames: [{ audioMuted: boolean, // frame is muted diff --git a/docs/windowActions.md b/docs/windowActions.md index a189051549d..be1f887b98d 100644 --- a/docs/windowActions.md +++ b/docs/windowActions.md @@ -277,6 +277,30 @@ Dispatches a message to the store to set the tab page index. +### setTabBreakpoint(frameProps, breakpoint) + +Dispatches a message to the store to set the tab breakpoint. + +**Parameters** + +**frameProps**: `Object`, the frame properties for the webview in question. + +**breakpoint**: `string`, the tab breakpoint to change to + + + +### setTabHoverState(frameProps, hoverState) + +Dispatches a message to the store to set the current tab hover state. + +**Parameters** + +**frameProps**: `Object`, the frame properties for the webview in question. + +**hoverState**: `boolean`, whether or not mouse is over tab + + + ### setPreviewTabPageIndex(previewTabPageIndex) Dispatches a message to the store to set the tab page index being previewed. diff --git a/js/actions/windowActions.js b/js/actions/windowActions.js index b03cceafd13..e8990a7cbc3 100644 --- a/js/actions/windowActions.js +++ b/js/actions/windowActions.js @@ -402,6 +402,34 @@ const windowActions = { }) }, + /** + * Dispatches a message to the store to set the tab breakpoint. + * + * @param {Object} frameProps - the frame properties for the webview in question. + * @param {string} breakpoint - the tab breakpoint to change to + */ + setTabBreakpoint: function (frameProps, breakpoint) { + dispatch({ + actionType: windowConstants.WINDOW_SET_TAB_BREAKPOINT, + frameProps, + breakpoint + }) + }, + + /** + * Dispatches a message to the store to set the current tab hover state. + * + * @param {Object} frameProps - the frame properties for the webview in question. + * @param {boolean} hoverState - whether or not mouse is over tab + */ + setTabHoverState: function (frameProps, hoverState) { + dispatch({ + actionType: windowConstants.WINDOW_SET_TAB_HOVER_STATE, + frameProps, + hoverState + }) + }, + /** * Dispatches a message to the store to set the tab page index being previewed. * diff --git a/js/components/tab.js b/js/components/tab.js index 21e04505349..1017c5f41c6 100644 --- a/js/components/tab.js +++ b/js/components/tab.js @@ -5,6 +5,7 @@ const React = require('react') const ImmutableComponent = require('./immutableComponent') +const {StyleSheet, css} = require('aphrodite') const windowActions = require('../actions/windowActions') const locale = require('../l10n') @@ -18,14 +19,19 @@ const contextMenus = require('../contextMenus') const dnd = require('../dnd') const windowStore = require('../stores/windowStore') const ipc = require('electron').ipcRenderer +const throttle = require('../lib/throttle') -const {TabIcon, AudioTabIcon} = require('../../app/renderer/components/tabIcon') +const styles = require('../../app/renderer/components/styles/tab') +const {Favicon, AudioTabIcon, NewSessionIcon, + PrivateIcon, TabTitle, CloseTabIcon} = require('../../app/renderer/components/tabContent') +const {getTabBreakpoint} = require('../../app/renderer/lib/tabUtil') class Tab extends ImmutableComponent { constructor () { super() this.onMouseEnter = this.onMouseEnter.bind(this) this.onMouseLeave = this.onMouseLeave.bind(this) + this.onUpdateTabSize = this.onUpdateTabSize.bind(this) } get frame () { return windowStore.getFrame(this.props.tab.get('frameKey')) @@ -123,8 +129,11 @@ class Tab extends ImmutableComponent { } onMouseLeave () { - window.clearTimeout(this.hoverTimeout) - windowActions.setPreviewFrame(null) + if (this.props.previewTabs) { + window.clearTimeout(this.hoverTimeout) + windowActions.setPreviewFrame(null) + } + windowActions.setTabHoverState(this.frame, false) } onMouseEnter (e) { @@ -135,8 +144,11 @@ class Tab extends ImmutableComponent { // If user isn't in previewMode, we add a bit of delay to avoid tab from flashing out // as reported here: https://github.com/brave/browser-laptop/issues/1434 - this.hoverTimeout = - window.setTimeout(windowActions.setPreviewFrame.bind(null, this.frame), previewMode ? 0 : 200) + if (this.props.previewTabs) { + this.hoverTimeout = + window.setTimeout(windowActions.setPreviewFrame.bind(null, this.frame), previewMode ? 0 : 200) + } + windowActions.setTabHoverState(this.frame, true) } onClickTab (e) { @@ -148,48 +160,69 @@ class Tab extends ImmutableComponent { } } - render () { - // Style based on theme-color - const iconSize = 16 - let iconStyle = { - minWidth: iconSize, - width: iconSize - } - const activeTabStyle = {} - const backgroundColor = this.props.paintTabs && (this.props.tab.get('themeColor') || this.props.tab.get('computedThemeColor')) - if (this.props.isActive && backgroundColor) { - activeTabStyle.background = backgroundColor - const textColor = getTextColorForBackground(backgroundColor) - iconStyle.color = textColor - if (textColor) { - activeTabStyle.color = getTextColorForBackground(backgroundColor) - } - } + get themeColor () { + return this.props.paintTabs && + (this.props.tab.get('themeColor') || this.props.tab.get('computedThemeColor')) + } - const icon = this.props.tab.get('icon') - const defaultIcon = 'fa fa-file-o' + get tabSize () { + const tab = this.tabNode + // Avoid TypeError keeping it null until component is mounted + return tab && !this.isPinned ? tab.getBoundingClientRect().width : null + } - if (!this.loading && icon) { - iconStyle = Object.assign(iconStyle, { - backgroundImage: `url(${icon})`, - backgroundSize: iconSize, - height: iconSize - }) - } + get narrowView () { + const sizes = ['medium', 'mediumSmall', 'small', 'extraSmall', 'smallest'] + return sizes.includes(this.props.tab.get('breakpoint')) + } - let playIcon = false - let iconClass = null - if (this.props.tab.get('audioPlaybackActive') || this.props.tab.get('audioMuted')) { - if (this.props.tab.get('audioPlaybackActive') && !this.props.tab.get('audioMuted')) { - iconClass = 'fa fa-volume-up' - } else if (this.props.tab.get('audioPlaybackActive') && this.props.tab.get('audioMuted')) { - iconClass = 'fa fa-volume-off' - } - playIcon = true + get narrowestView () { + const sizes = ['extraSmall', 'smallest'] + return sizes.includes(this.props.tab.get('breakpoint')) + } + + get canPlayAudio () { + return this.props.tab.get('audioPlaybackActive') || this.props.tab.get('audioMuted') + } + + onUpdateTabSize () { + const currentSize = getTabBreakpoint(this.tabSize) + // Avoid changing state on unmounted component + // when user switch to a new tabSet + if (this.tabNode) { + windowActions.setTabBreakpoint(this.frame, currentSize) } + } + + componentWillMount () { + this.onUpdateTabSize() + } - const locationHasFavicon = this.props.tab.get('location') !== 'about:newtab' + componentDidMount () { + // Execute resize handler at a rate of 15fps + window.addEventListener('resize', throttle(this.onUpdateTabSize, 66)) + } + componentDidUpdate () { + this.onUpdateTabSize() + } + + componentWillUnmount () { + this.onUpdateTabSize() + window.removeEventListener('resize', this.onUpdateTabSize) + } + + render () { + const perPageStyles = StyleSheet.create({ + themeColor: { + color: this.themeColor ? getTextColorForBackground(this.themeColor) : 'inherit', + background: this.themeColor ? this.themeColor : 'inherit', + ':hover': { + color: this.themeColor ? getTextColorForBackground(this.themeColor) : 'inherit', + background: this.themeColor ? this.themeColor : 'inherit' + } + } + }) return
-
+
{ this.tabNode = node }} draggable @@ -216,51 +257,31 @@ class Tab extends ImmutableComponent { onDragEnd={this.onDragEnd.bind(this)} onDragOver={this.onDragOver.bind(this)} onClick={this.onClickTab.bind(this)} - onContextMenu={contextMenus.onTabContextMenu.bind(this, this.frame)} - style={activeTabStyle}> - { - this.props.tab.get('isPrivate') - ? - : null - } - { - this.props.tab.get('partitionNumber') - ? - : null - } - { - locationHasFavicon - ?
- : null - } - { - playIcon - ? - : null - } - { - !this.isPinned - ?
- {this.displayValue} -
- : null - } - { - !this.isPinned - ? - : null - } + onContextMenu={contextMenus.onTabContextMenu.bind(this, this.frame)}> +
+ + + +
+ + +
} diff --git a/js/components/tabsToolbar.js b/js/components/tabsToolbar.js index d6723051625..429516aaf2e 100644 --- a/js/components/tabsToolbar.js +++ b/js/components/tabsToolbar.js @@ -65,6 +65,7 @@ class TabsToolbar extends ImmutableComponent { tabsPerTabPage={this.props.tabsPerTabPage} activeFrameKey={this.props.activeFrameKey} tabPageIndex={this.props.tabPageIndex} + tabBreakpoint={this.props.tabBreakpoint} currentTabs={currentTabs} previewTabPageIndex={this.props.previewTabPageIndex} startingFrameIndex={startingFrameIndex} diff --git a/js/constants/windowConstants.js b/js/constants/windowConstants.js index 62b17ab20d6..9ace09ec242 100644 --- a/js/constants/windowConstants.js +++ b/js/constants/windowConstants.js @@ -16,6 +16,8 @@ const windowConstants = { WINDOW_SET_PREVIEW_FRAME: _, WINDOW_SET_PREVIEW_TAB_PAGE_INDEX: _, WINDOW_SET_TAB_PAGE_INDEX: _, + WINDOW_SET_TAB_BREAKPOINT: _, + WINDOW_SET_TAB_HOVER_STATE: _, WINDOW_SET_IS_BEING_DRAGGED_OVER_DETAIL: _, WINDOW_TAB_MOVE: _, WINDOW_SET_THEME_COLOR: _, diff --git a/js/lib/throttle.js b/js/lib/throttle.js new file mode 100644 index 00000000000..d951c5cf00f --- /dev/null +++ b/js/lib/throttle.js @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict' + +function throttle (fn, limit) { + let waitingTime = false + return () => { + if (!waitingTime) { + fn.call() + waitingTime = true + setTimeout(() => { + waitingTime = false + }, limit) + } + } +} + +module.exports = throttle diff --git a/js/stores/windowStore.js b/js/stores/windowStore.js index 99b3f97fcd9..b532df16d00 100644 --- a/js/stores/windowStore.js +++ b/js/stores/windowStore.js @@ -396,6 +396,14 @@ const doAction = (action) => { updateTabPageIndex(action.frameProps) } break + case windowConstants.WINDOW_SET_TAB_BREAKPOINT: + windowState = windowState.setIn(['frames', frameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps), 'breakpoint'], action.breakpoint) + windowState = windowState.setIn(['tabs', frameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps), 'breakpoint'], action.breakpoint) + break + case windowConstants.WINDOW_SET_TAB_HOVER_STATE: + windowState = windowState.setIn(['frames', frameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps), 'hoverState'], action.hoverState) + windowState = windowState.setIn(['tabs', frameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps), 'hoverState'], action.hoverState) + break case windowConstants.WINDOW_SET_IS_BEING_DRAGGED_OVER_DETAIL: if (!action.dragOverKey) { windowState = windowState.deleteIn(['ui', 'dragging']) diff --git a/less/tabs.less b/less/tabs.less index bc403ff4f7c..4776c65af83 100644 --- a/less/tabs.less +++ b/less/tabs.less @@ -4,19 +4,6 @@ @import "variables.less"; -// Windows specific styles -.platform--win32 { - .tab { - color: #555; - - .tabTitle { - font-weight: 500; - font-size: 12px; - height: 18px; - } - } -} - .tabs { box-sizing: border-box; background: none; @@ -68,121 +55,6 @@ } } -.tab { - background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1)); - border-radius: @borderRadiusTabs @borderRadiusTabs 0px 0px; - border-width: 1px 1px 0; - box-sizing: border-box; - color: #3B3B3B; - display: flex; - height: 23px; - margin-top: 2px; - transition: transform 200ms ease; - left: 0; - opacity: 1.0; - padding: 0; - width: 100%; - align-items: center; - justify-content: center; - border: 1px solid rgba(0, 0, 0, 0.0); - border-bottom: 1px; - - .tabTitle { - -webkit-user-select: none; - box-sizing: border-box; - display: inline-block; - font-size: 12px; - overflow: hidden; - text-overflow: ellipsis; - line-height: 16px; - white-space: nowrap; - vertical-align: middle; - width: calc(~'100% - 40px'); - height: 15px; - margin-left: 7px; - } - .tabIcon { - background-position: center; - background-repeat: no-repeat; - display: inline-block; - font-size: 12px; - text-align: center; - margin-left: 7px; - } - - .thumbnail { - display: none; - position: absolute; - top: 32px; - left: 0; - border: 1px solid #000; - padding: 10px; - background: #fff; - pointer-events: none; - z-index: @zindexTabsThumbnail; - } - - &.active { - background: linear-gradient(to bottom, white, @chromePrimary, ); - height: 25px; - margin-top: 1px; - box-shadow: inset 1px 1px 2px 0px white; - box-shadow: 0px -1px 4px 0px rgba(51, 51, 51, 0.12); - border: 1px solid #bbb; - border-bottom: 1px; - } - - &.private { - background: @privateTabBackground; - color: #fff; - &:not(.active) { - background: lighten(@privateTabBackground, 40%); - } - } - - &:hover { - .closeTab { - opacity: 0.5; - } - - .thumbnail { - display: block; - } - } - - &:not(.active):hover { - background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(250, 250, 250, 0.4)); - &.private { - background: lighten(@privateTabBackground, 20%); - } - } - - &.dragging { - &:hover { - .closeTab { - opacity: 0; - } - } - } - - .closeTab { - opacity: 0; - text-align: center; - width: 16px; - height: 16px; - margin-left: 4px; - margin-right: 4px; - - &:hover { - opacity: 1; - } - - border: 0px solid white; - border-radius: 50%; - z-index: @zindexTabs; - } -} - .tabArea { box-sizing: border-box; display: inline-block; diff --git a/test/unit/app/renderer/tabContentTest.js b/test/unit/app/renderer/tabContentTest.js new file mode 100644 index 00000000000..032055db560 --- /dev/null +++ b/test/unit/app/renderer/tabContentTest.js @@ -0,0 +1,380 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* global describe, before, after, it */ + +const mockery = require('mockery') +const {shallow} = require('enzyme') +const Immutable = require('immutable') +const assert = require('assert') +const fakeElectron = require('../../lib/fakeElectron') +const globalStyles = require('../../../../app/renderer/components/styles/global') +let Favicon, AudioTabIcon, PrivateIcon, NewSessionIcon, TabTitle, CloseTabIcon +require('../../braveUnit') + +describe('tabContent components', function () { + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + Favicon = require('../../../../app/renderer/components/tabContent').Favicon + AudioTabIcon = require('../../../../app/renderer/components/tabContent').AudioTabIcon + PrivateIcon = require('../../../../app/renderer/components/tabContent').PrivateIcon + NewSessionIcon = require('../../../../app/renderer/components/tabContent').NewSessionIcon + TabTitle = require('../../../../app/renderer/components/tabContent').TabTitle + CloseTabIcon = require('../../../../app/renderer/components/tabContent').CloseTabIcon + }) + after(function () { + mockery.disable() + }) + + const url1 = 'https://brave.com' + const favicon1 = 'https://brave.com/favicon.ico' + const pageTitle1 = 'Brave Software' + + describe('Favicon', function () { + it('should show favicon if page has one', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().favicon, favicon1) + }) + it('should show a placeholder icon if page has no favicon', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.defaultIcon) + }) + it('should show a loading icon if page is still loading', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.loading) + }) + it('should not show favicon for new tab page', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().favicon, favicon1, 'does not show favicon') + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.loading, 'does not show loading icon') + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.defaultIcon, 'does not show default icon') + }) + }) + + describe('AudioTabIcon', function () { + it('should not show any audio icon if page has audio disabled', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.volumeOn) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.volumeOff) + }) + it('should show play icon if page has audio enabled', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.volumeOn) + }) + it('should not show play audio icon if tab size is too narrow', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.volumeOn) + }) + it('should show mute icon if page has audio muted', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.volumeOff) + }) + it('should not show mute icon if tab size is too narrow', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.volumeOff) + }) + }) + + describe('PrivateIcon', function () { + it('should show private icon if current tab is private', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.private) + }) + it('should not show private icon if current tab is not private', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.private) + }) + it('should not show private icon if mouse is over tab (avoid icon overflow)', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.private) + }) + it('should not show private icon if tab size is too small', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.private) + }) + }) + + describe('NewSessionIcon', function () { + it('should show new session icon if current tab is a new session tab', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.newSession) + }) + it('should not show new session icon if current tab is not private', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.newSession) + }) + it('should not show new session icon if mouse is over tab (avoid icon overflow)', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.newSession) + }) + it('should not show new session icon if tab size is too small', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.newSession) + }) + }) + + describe('Tab Title', function () { + it('should show text if page has a title', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.text(), pageTitle1) + }) + it('should not show text if tab is pinned', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.text(), pageTitle1) + }) + it('should not show text if size is largeMedium and location has audio and a secondary icon', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.text(), pageTitle1) + }) + it('should not show text if size is mediumSmall and location has a secondary icon', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.text(), pageTitle1) + }) + it('should not show text if size is too small', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.text(), pageTitle1) + }) + }) + + describe('CloseTabIcon', function () { + it('should show closeTab icon if mouse is over tab', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.closeTab) + }) + it('should not show closeTab icon if mouse is not over a tab', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.closeTab) + }) + it('should not show closeTab icon if tab is pinned', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.closeTab) + }) + it('should not show closeTab icon if tab size is too small', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.closeTab) + }) + }) +}) diff --git a/test/unit/app/renderer/tabIconTest.js b/test/unit/app/renderer/tabIconTest.js deleted file mode 100644 index c0ea4596158..00000000000 --- a/test/unit/app/renderer/tabIconTest.js +++ /dev/null @@ -1,50 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* global describe, before, after, it */ - -const mockery = require('mockery') -const {mount, shallow} = require('enzyme') -const assert = require('assert') -const sinon = require('sinon') -const fakeElectron = require('../../lib/fakeElectron') -let TabIcon, AudioTabIcon -require('../../braveUnit') - -describe('tabIcon component', function () { - before(function () { - mockery.enable({ - warnOnReplace: false, - warnOnUnregistered: false, - useCleanCache: true - }) - mockery.registerMock('electron', fakeElectron) - TabIcon = require('../../../../app/renderer/components/tabIcon').TabIcon - AudioTabIcon = require('../../../../app/renderer/components/tabIcon').AudioTabIcon - }) - after(function () { - mockery.disable() - }) - - describe('TabIcon', function () { - it('should call onClick callback', function () { - const onClick = sinon.spy() - const wrapper = shallow() - wrapper.find('div').simulate('click') - assert(onClick.calledOnce) - }) - }) - - describe('AudioTabIcon', function () { - it('should call onClick callback', function () { - const onClick = sinon.spy() - const wrapper = mount() - wrapper.find('div').simulate('click') - assert(onClick.calledOnce) - }) - it('should render a TabIcon with withBlueIcon prop', function () { - const wrapper = mount() - assert.ok(wrapper.find(TabIcon).props().withBlueIcon) - }) - }) -})