diff --git a/app/renderer/components/styles/global.js b/app/renderer/components/styles/global.js index 6a28d81c039..6761ad9bded 100644 --- a/app/renderer/components/styles/global.js +++ b/app/renderer/components/styles/global.js @@ -91,7 +91,9 @@ const globalStyles = { navbarBraveButtonMarginLeft: '80px', navbarLeftMarginDarwin: '76px', sideBarWidth: '190px', - aboutPageSectionPadding: '24px' + aboutPageSectionPadding: '24px', + defaultIconPadding: '0 2px', + defaultTabPadding: '0 4px' }, shadow: { switchShadow: 'inset 0 1px 4px rgba(0, 0, 0, 0.35)', diff --git a/app/renderer/components/styles/tab.js b/app/renderer/components/styles/tab.js new file mode 100644 index 00000000000..c3ea5c0f63f --- /dev/null +++ b/app/renderer/components/styles/tab.js @@ -0,0 +1,16 @@ +/* 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 tabStyles = { + breakpoint: { + medium: '83px', + small: '66px', + extraSmall: '53px', + hiddenSecondaryIcon: '42px', + faviconOnly: '33px', + hiddenFavicon: '16px' + } +} + +module.exports = tabStyles diff --git a/app/renderer/components/tabIcon.js b/app/renderer/components/tabIcon.js index 00d6b5174d7..ad2a9085d16 100644 --- a/app/renderer/components/tabIcon.js +++ b/app/renderer/components/tabIcon.js @@ -29,12 +29,13 @@ const styles = StyleSheet.create({ 'icon': { backgroundPosition: 'center', backgroundRepeat: 'no-repeat', - display: 'inline-block', + display: 'flex', fontSize: '14px', - margin: 'auto 7px auto 7px', position: 'relative', - verticalAlign: 'middle', - textAlign: 'center' + textAlign: 'center', + justifyContent: 'center', + width: '16px', + padding: globalStyles.spacing.defaultIconPadding }, 'blueIcon': { color: globalStyles.color.highlightBlue diff --git a/js/components/tab.js b/js/components/tab.js index 20ef799e872..62fdacd2a71 100644 --- a/js/components/tab.js +++ b/js/components/tab.js @@ -4,8 +4,6 @@ const React = require('react') -const ImmutableComponent = require('./immutableComponent') - const windowActions = require('../actions/windowActions') const locale = require('../l10n') const dragTypes = require('../constants/dragTypes') @@ -18,14 +16,20 @@ const contextMenus = require('../contextMenus') const dnd = require('../dnd') const windowStore = require('../stores/windowStore') const ipc = require('electron').ipcRenderer +const throttle = require('../lib/throttle') +const tabStyles = require('../../app/renderer/components/styles/tab') const {TabIcon, AudioTabIcon} = require('../../app/renderer/components/tabIcon') -class Tab extends ImmutableComponent { +class Tab extends React.Component { constructor () { super() this.onMouseEnter = this.onMouseEnter.bind(this) this.onMouseLeave = this.onMouseLeave.bind(this) + this.onUpdateTabSize = this.onUpdateTabSize.bind(this) + this.state = { + tabWidth: this.tabSize + } } get frame () { return windowStore.getFrame(this.props.tab.get('frameKey')) @@ -34,6 +38,27 @@ class Tab extends ImmutableComponent { return !!this.props.tab.get('pinnedLocation') } + get tabBreakpoint () { + const tabWidth = this.state.tabWidth + + const medium = tabWidth <= Number.parseInt(tabStyles.breakpoint.medium, 10) + const small = tabWidth <= Number.parseInt(tabStyles.breakpoint.small, 10) + const extraSmall = tabWidth <= Number.parseInt(tabStyles.breakpoint.extraSmall, 10) + const hiddenSecondaryIcon = tabWidth <= Number.parseInt(tabStyles.breakpoint.hiddenSecondaryIcon, 10) + const faviconOnly = tabWidth <= Number.parseInt(tabStyles.breakpoint.faviconOnly, 10) + const hiddenFavicon = tabWidth <= Number.parseInt(tabStyles.breakpoint.hiddenFavicon, 10) + + return { + medium, small, extraSmall, hiddenSecondaryIcon, faviconOnly, hiddenFavicon + } + } + + get tabSize () { + const tab = this.tabNode + // Avoid TypeError keeping it null until component is mounted + return tab && !this.isPinned ? tab.getBoundingClientRect().width : null + } + get draggingOverData () { if (!this.props.draggingOverData || this.props.draggingOverData.get('dragOverKey') !== this.props.tab.get('frameKey')) { @@ -52,6 +77,26 @@ class Tab extends ImmutableComponent { return this.props.draggingOverData } + onUpdateTabSize () { + // Avoid calling setState on unmounted component + // when user switch to a new tabSet + if (this.tabNode) this.setState({tabWidth: this.tabSize}) + } + + componentWillMount () { + this.onUpdateTabSize() + } + + componentDidMount () { + this.onUpdateTabSize() + // Execute resize handler at a rate of 15fps + window.addEventListener('resize', throttle(this.onUpdateTabSize, 66)) + } + + componentWillUnmount () { + window.removeEventListener('resize', this.onUpdateTabSize) + } + get isDragging () { const sourceDragData = dnd.getInProcessDragData() return sourceDragData && this.props.tab.get('frameKey') === sourceDragData.get('key') @@ -148,6 +193,14 @@ class Tab extends ImmutableComponent { } render () { + const breakpoint = this.tabBreakpoint + const narrowView = breakpoint.extraSmall || breakpoint.hiddenSecondaryIcon || breakpoint.faviconOnly || breakpoint.hiddenFavicon + const secondaryIconIsVisible = !breakpoint.hiddenSecondaryIcon && !breakpoint.faviconOnly && !breakpoint.hiddenFavicon + + let privateIconStyle = narrowView ? {padding: '0'} : null + let tabIdStyle = narrowView ? {justifyContent: 'center'} : null + let closeTabStyle = {} + // Style based on theme-color const iconSize = 16 let iconStyle = { @@ -165,6 +218,8 @@ class Tab extends ImmutableComponent { } } + const locationHasPrivateIcon = !!this.props.tab.get('isPrivate') || !!this.props.tab.get('partitionNumber') + const icon = this.props.tab.get('icon') const defaultIcon = 'fa fa-file-o' @@ -176,19 +231,52 @@ class Tab extends ImmutableComponent { }) } + if (narrowView) { + closeTabStyle = Object.assign(closeTabStyle, { + right: '0' + }) + iconStyle = Object.assign(iconStyle, { + padding: '0' + }) + } + + if (breakpoint.faviconOnly && this.props.isActive) { + Object.assign(closeTabStyle, { + opacity: '1', + width: '100%', + padding: '0', + backgroundColor: 'white', + borderTopLeftRadius: '4px', + borderTopRightRadius: '4px' + }) + } + + const playIconExists = !!this.props.tab.get('audioPlaybackActive') || !!this.props.tab.get('audioMuted') + let playIcon = false let iconClass = null - if (this.props.tab.get('audioPlaybackActive') || this.props.tab.get('audioMuted')) { + if (playIconExists) { 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 + // We don't want playIcon to be shown on small tabs + playIcon = !narrowView && !(breakpoint.small && locationHasPrivateIcon) + console.log('nao ta narrow', playIcon) } const locationHasFavicon = this.props.tab.get('location') !== 'about:newtab' + const audioPlayNarrowView = playIconExists && ((breakpoint.small && locationHasPrivateIcon) || narrowView) + const privateIcon = this.props.tab.get('isPrivate') && secondaryIconIsVisible + const newSessionIcon = this.props.tab.get('partitionNumber') && secondaryIconIsVisible + const closeTabButton = !this.isPinned && (!breakpoint.faviconOnly && !breakpoint.hiddenFavicon) || + (breakpoint.faviconOnly && this.props.isActive) + const isHiddenTitle = ((breakpoint.medium || breakpoint.small) && playIconExists && locationHasPrivateIcon) || + (breakpoint.extraSmall && locationHasPrivateIcon) || + breakpoint.faviconOnly || breakpoint.hiddenFavicon + return <div className={cx({ tabArea: true, @@ -204,7 +292,9 @@ class Tab extends ImmutableComponent { tab: true, isPinned: this.isPinned, active: this.props.isActive, - private: this.props.tab.get('isPrivate') + private: this.props.tab.get('isPrivate'), + noFavicon: !locationHasFavicon, + alternativePlayIndicator: audioPlayNarrowView })} data-frame-key={this.props.tab.get('frameKey')} ref={(node) => { this.tabNode = node }} @@ -216,47 +306,51 @@ class Tab extends ImmutableComponent { onClick={this.onClickTab.bind(this)} onContextMenu={contextMenus.onTabContextMenu.bind(this, this.frame)} style={activeTabStyle}> + <div className='tabId' style={tabIdStyle}> + { + (locationHasFavicon && !breakpoint.hiddenFavicon) || this.isPinned + ? <div className={cx({ + tabIcon: true, + bookmarkFile: !icon, + [defaultIcon]: !icon, + 'fa fa-circle-o-notch fa-spin': this.loading + })} + style={iconStyle} /> + : null + } + { + !this.isPinned && !isHiddenTitle + ? <div className='tabTitle'> + {this.displayValue} + </div> + : null + } + { + playIcon + ? <AudioTabIcon styles={iconClass} + onClick={this.onMuteFrame.bind(this, !this.props.tab.get('audioMuted'))} /> + : null + } + </div> { - this.props.tab.get('isPrivate') - ? <TabIcon styles='fa fa-eye' /> + privateIcon + ? <TabIcon styles='fa fa-eye' style={privateIconStyle} /> : null } { - this.props.tab.get('partitionNumber') + newSessionIcon ? <TabIcon l10nArgs={JSON.stringify({partitionNumber: this.props.tab.get('partitionNumber')})} l10nId='sessionInfoTab' - styles='fa fa-user' /> - : null - } - { - locationHasFavicon - ? <div className={cx({ - tabIcon: true, - bookmarkFile: !icon, - [defaultIcon]: !icon, - 'fa fa-circle-o-notch fa-spin': this.loading - })} - style={iconStyle} /> - : null - } - { - playIcon - ? <AudioTabIcon styles={iconClass} - onClick={this.onMuteFrame.bind(this, !this.props.tab.get('audioMuted'))} /> - : null - } - { - !this.isPinned - ? <div className='tabTitle'> - {this.displayValue} - </div> + styles='fa fa-user' + style={privateIconStyle} /> : null } { - !this.isPinned + closeTabButton ? <span onClick={this.onCloseFrame.bind(this)} data-l10n-id='closeTabButton' - className='closeTab fa fa-times-circle' /> + className='closeTab fa fa-times-circle' + style={closeTabStyle} /> : null } </div> 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/less/tabs.less b/less/tabs.less index bc403ff4f7c..34eab249f70 100644 --- a/less/tabs.less +++ b/less/tabs.less @@ -83,31 +83,47 @@ padding: 0; width: 100%; align-items: center; - justify-content: center; + justify-content: space-between; border: 1px solid rgba(0, 0, 0, 0.0); border-bottom: 1px; + padding: @defaultTabPadding; + position: relative; + + // Add extra space for pages that have no icon + // such as about:blank and about:newtab + &.noFavicon { + padding: 0 6px; + } + + &.alternativePlayIndicator { + border-top: 2px solid lightskyblue !important; + } + + .tabId { + align-items: center; + display: flex; + flex: 1; + min-width: 0; // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1108514#c5 + } .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; + padding: @defaultTabPadding; } + .tabIcon { background-position: center; background-repeat: no-repeat; display: inline-block; font-size: 12px; text-align: center; - margin-left: 7px; + padding: @defaultIconPadding; } .thumbnail { @@ -122,6 +138,10 @@ z-index: @zindexTabsThumbnail; } + &.isPinned { + padding: @defaultIconPadding; + } + &.active { background: linear-gradient(to bottom, white, @chromePrimary, ); height: 25px; @@ -142,16 +162,21 @@ &:hover { .closeTab { - opacity: 0.5; + opacity: 1; } .thumbnail { display: block; } + + // Add opacity to tab content so closeTab has more focus + .tabId, .privateIcon, .audioPlaybackActive { + opacity: 0.5; + } } &:not(.active):hover { - background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(250, 250, 250, 0.4)); + background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(250, 250, 250, 0.4), ); &.private { background: lighten(@privateTabBackground, 20%); } @@ -167,11 +192,17 @@ .closeTab { opacity: 0; - text-align: center; + position: absolute; + top: 0; + right: 4px; + border-radius: 0; + border-top-right-radius: @borderRadius; + display: flex; + align-items: center; + justify-content: center; width: 16px; - height: 16px; - margin-left: 4px; - margin-right: 4px; + height: 100%; + padding: @defaultIconPadding; &:hover { opacity: 1; diff --git a/less/variables.less b/less/variables.less index e4506ed387d..58b9d7c9788 100644 --- a/less/variables.less +++ b/less/variables.less @@ -147,6 +147,8 @@ @transitionDuration: 100ms; @aboutPageSectionPadding: 24px; +@defaultIconPadding: 0 2px; +@defaultTabPadding: 0 4px; @transition: all 600ms linear; @transitionFast: all 100ms linear;