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.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)
- })
- })
-})