diff --git a/src/examples/src/config.tsx b/src/examples/src/config.tsx index 62ff93e6a7..267e84a67a 100644 --- a/src/examples/src/config.tsx +++ b/src/examples/src/config.tsx @@ -193,6 +193,7 @@ import TitleText from './widgets/speed-dial/TitleText'; import BasicSwitch from './widgets/switch/Basic'; import DisabledSwitch from './widgets/switch/Disabled'; import BasicTabContainer from './widgets/tab-container/Basic'; +import VariableWidthTabContainer from './widgets/tab-container/VariableWidth'; import ControlledTabContainer from './widgets/tab-container/Controlled'; import ButtonAlignmentTabContainer from './widgets/tab-container/ButtonAlignment'; import CloseableTabContainer from './widgets/tab-container/Closeable'; @@ -1801,6 +1802,11 @@ export const config = { filename: 'Closeable', module: CloseableTabContainer, title: 'TabContainer with closeable tab' + }, + { + filename: 'VariableWidth', + module: VariableWidthTabContainer, + title: 'TabContainer with scrollable variable width tabs' } ], filename: 'index', diff --git a/src/examples/src/widgets/tab-container/VariableWidth.tsx b/src/examples/src/widgets/tab-container/VariableWidth.tsx new file mode 100644 index 0000000000..95e827e37e --- /dev/null +++ b/src/examples/src/widgets/tab-container/VariableWidth.tsx @@ -0,0 +1,35 @@ +import { tsx, create } from '@dojo/framework/core/vdom'; + +import TabContainer from '@dojo/widgets/tab-container'; +import Example from '../../Example'; +import Button from '@dojo/widgets/button'; + +const factory = create(); + +export default factory(function VariableWidth() { + const tabs = [ + { name: 'One' }, + { name: 'Tab Two' }, + { name: 'Long Tab Three' }, + { name: "Tab Four's Name Is Beyond Max Length Causing Ellipsis" } + ]; + + return ( + + +
+ +
+
Hello Tab Two
+
Hello Tab Three
+
Hello Tab Four
+
+
+ ); +}); diff --git a/src/tab-container/index.tsx b/src/tab-container/index.tsx index d36361a6a1..5a4affda7d 100644 --- a/src/tab-container/index.tsx +++ b/src/tab-container/index.tsx @@ -9,6 +9,7 @@ import bundle from './nls/TabContainer'; import { formatAriaProperties, Keys } from '../common/util'; import * as css from '../theme/default/tab-container.m.css'; import { AriaAttributes } from '@dojo/framework/core/interfaces'; +import offscreen from '../middleware/offscreen'; export interface TabItem { closeable?: boolean; @@ -31,23 +32,27 @@ export interface TabContainerProperties { onActiveIndex?(index: number): void; /** Tabs config used to display tab buttons */ tabs: TabItem[]; + /** If buttons should be fixed width or variable width */ + fixed?: boolean; } interface TabContainerICache { activeIndex: number | undefined; + horizontalScrollbarHeight: number | undefined; } const factory = create({ focus, i18n, icache: createICacheMiddleware(), - theme + theme, + offscreen }).properties(); export const TabContainer = factory(function TabContainer({ children, id, - middleware: { focus, i18n, icache, theme }, + middleware: { focus, i18n, icache, theme, offscreen }, properties }) { const { @@ -59,7 +64,8 @@ export const TabContainer = factory(function TabContainer({ onClose, theme: themeProp, classes, - variant + variant, + fixed = true } = properties(); let { activeIndex } = properties(); @@ -177,10 +183,31 @@ export const TabContainer = factory(function TabContainer({ ); }; - const content = [ -
- {tabs.map(renderTab)} -
, + const renderedTabs = tabs.map(renderTab); + + const horizontalScrollbarHeight = icache.getOrSet('horizontalScrollbarHeight', () => { + return offscreen( + () =>
, + (node) => node.offsetHeight - node.clientHeight + ); + }); + + let content = [ + fixed ? ( +
+ {...renderedTabs} +
+ ) : ( +
+
+ {...renderedTabs} +
+
+ ),
{children().map((child, index) => { const disabled = tabs[index].disabled; @@ -218,7 +245,12 @@ export const TabContainer = factory(function TabContainer({ {...formatAriaProperties(aria)} key="root" aria-orientation={orientation} - classes={[theme.variant(), alignClass || null, themeCss.root]} + classes={[ + theme.variant(), + alignClass || null, + themeCss.root, + fixed ? themeCss.fixed : themeCss.scroller + ]} role="tablist" > {...content} diff --git a/src/tab-container/tests/TabContainer.spec.tsx b/src/tab-container/tests/TabContainer.spec.tsx index d75b0163ca..28ff58a066 100644 --- a/src/tab-container/tests/TabContainer.spec.tsx +++ b/src/tab-container/tests/TabContainer.spec.tsx @@ -1,11 +1,12 @@ -import { AriaAttributes } from '@dojo/framework/core/interfaces'; +import { AriaAttributes, RenderResult } from '@dojo/framework/core/interfaces'; const { registerSuite } = intern.getInterface('object'); const { assert } = intern.getPlugin('chai'); import * as sinon from 'sinon'; -import { tsx, v, w } from '@dojo/framework/core/vdom'; +import { create, tsx, v, w } from '@dojo/framework/core/vdom'; +import assertionTemplate from '@dojo/framework/testing/harness/assertionTemplate'; import { Keys } from '../../common/util'; import Icon from '../../icon'; @@ -18,7 +19,7 @@ import { isStringComparator, noop } from '../../common/tests/support/test-helpers'; -import assertionTemplate from '@dojo/framework/testing/harness/assertionTemplate'; +import offscreen from '../../middleware/offscreen'; const compareLabelledBy = { selector: '*', property: 'labelledBy', comparator: isStringComparator }; const compareControls = { selector: '*', property: 'controls', comparator: isStringComparator }; @@ -41,7 +42,7 @@ const baseTemplate = assertionTemplate(() => (
@@ -49,11 +50,29 @@ const baseTemplate = assertionTemplate(() => (
)); +const scrollableBaseTemplate = assertionTemplate(() => ( +
+
+
+
+
+
+)); + const reverseOrientationTemplate = assertionTemplate(() => (
@@ -427,6 +446,77 @@ registerSuite('TabContainer', { h.trigger('@1-tabbutton', 'onclick'); h.expect(disabledTemplate); assert.isTrue(onActiveIndexStub.notCalled); + }, + + 'variable width tabs'() { + const tabs = [{ name: 'tab0' }, { name: 'tab1' }]; + + let offscreenRenderResult: RenderResult; + + const factory = create(); + const offscreenMock = factory(function offscreen() { + return ( + renderFunction: () => RenderResult, + predicate: (node: HTMLDivElement) => number + ): number => { + offscreenRenderResult = renderFunction(); + return predicate({ offsetHeight: 10, clientHeight: 5 } as any); + }; + }); + + const h = harness( + () => ( + +
tab0
+
tab1
+
+ ), + { + middleware: [[offscreen, offscreenMock]] + } + ); + + const template = scrollableBaseTemplate + .setChildren('@buttons', () => [ +
+ + tab0 + + + + +
, +
+ + tab1 + + + + +
+ ]) + .setChildren('@tabs', () => [ + , + + ]); + + h.expect(template); + + assert.deepEqual(offscreenRenderResult,
); } } }); diff --git a/src/theme/default/tab-container.m.css b/src/theme/default/tab-container.m.css index 6482dffb1e..ffa06c0649 100644 --- a/src/theme/default/tab-container.m.css +++ b/src/theme/default/tab-container.m.css @@ -62,6 +62,46 @@ box-sizing: border-box; } +/* Added to an TabButton */ +.fixed { +} + +.scroller { + overflow-y: hidden; +} + +.scrollArea { + -webkit-overflow-scrolling: touch; + display: flex; + overflow-x: hidden; +} + +.scroll { + overflow-x: scroll; +} + +.scrollTest { + position: absolute; + top: -9999px; + width: 100px; + height: 100px; + overflow-x: scroll; +} + +.scrollArea::-webkit-scrollbar, +.scrollTest::-webkit-scrollbar { + display: none; +} + +.scrollContent { + position: relative; + display: flex; + flex: 1 0 auto; + -webkit-transform: none; + transform: none; + will-change: transform; +} + /* The root class of the TabButton */ .tabButton { border: 1px solid transparent; @@ -78,6 +118,12 @@ top: 1px; } +.scroller .tabButton { + min-width: 90px; + max-width: 360px; + flex-grow: 0; +} + /* Focus for a TabButton when not disabled */ .tabButton:focus:not(.disabledTabButton) { font-weight: bold; diff --git a/src/theme/default/tab-container.m.css.d.ts b/src/theme/default/tab-container.m.css.d.ts index 863161b570..0ad4432e1e 100644 --- a/src/theme/default/tab-container.m.css.d.ts +++ b/src/theme/default/tab-container.m.css.d.ts @@ -8,6 +8,12 @@ export const tabButtonContent: string; export const activeTabButtonLabel: string; export const activeTabButton: string; export const root: string; +export const fixed: string; +export const scroller: string; +export const scrollArea: string; +export const scroll: string; +export const scrollTest: string; +export const scrollContent: string; export const tabButton: string; export const disabledTabButton: string; export const alignLeft: string; diff --git a/src/theme/dojo/tab-container.m.css b/src/theme/dojo/tab-container.m.css index 94684d2cdb..c03b780b67 100644 --- a/src/theme/dojo/tab-container.m.css +++ b/src/theme/dojo/tab-container.m.css @@ -6,10 +6,21 @@ font-family: var(--font-family); } +.root:not(.fixed) .tabButton { + min-width: 90px; + max-width: 360px; + flex-grow: 0; +} + .tabButtons { display: flex; } +.root.fixed .tabButton { + width: var(--tab-width); + flex: 1; +} + .tabButton { border-bottom: var(--border-width) solid var(--tab-button-disabled-color); border-left: var(--border-width) solid transparent; @@ -17,21 +28,25 @@ border-top: var(--border-width) solid transparent; color: var(--tab-button-color); cursor: pointer; - display: inline-block; - flex: 1; + display: flex; outline: none; overflow: hidden; padding: calc(var(--grid-base) * 2) calc(var(--grid-base) / 2); position: relative; - text-align: center; + justify-content: center; text-overflow: ellipsis; vertical-align: top; white-space: nowrap; - width: var(--tab-width); margin: 0; background-color: var(--tab-button-background); } +.tabButtonContent { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .tabButton:hover:not(.disabledTabButton):not(.activeTabButton) { background-color: var(--color-background-faded); border-color: var(--color-background-faded); diff --git a/src/theme/dojo/tab-container.m.css.d.ts b/src/theme/dojo/tab-container.m.css.d.ts index aa20c2bb68..eab9050770 100644 --- a/src/theme/dojo/tab-container.m.css.d.ts +++ b/src/theme/dojo/tab-container.m.css.d.ts @@ -1,6 +1,8 @@ export const root: string; -export const tabButtons: string; +export const fixed: string; export const tabButton: string; +export const tabButtons: string; +export const tabButtonContent: string; export const disabledTabButton: string; export const activeTabButton: string; export const activeTabButtonLabel: string; diff --git a/src/theme/material/tab-container.m.css b/src/theme/material/tab-container.m.css index 2392677f7d..310685d605 100644 --- a/src/theme/material/tab-container.m.css +++ b/src/theme/material/tab-container.m.css @@ -9,12 +9,21 @@ pointer-events: all; } +.root:not(.fixed) .tabButton { + min-width: 90px; + max-width: 360px; + flex-grow: 0; +} + .tabButton:not(.activeTabButton) .tabButtonContent { color: var(--mdc-tab-button-text-color); } .tabButtonContent { composes: mdc-tab__text-label from '@material/tab/dist/mdc.tab.css'; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .activeTabButton { diff --git a/src/theme/material/tab-container.m.css.d.ts b/src/theme/material/tab-container.m.css.d.ts index 0de48c66aa..c42b9ce725 100644 --- a/src/theme/material/tab-container.m.css.d.ts +++ b/src/theme/material/tab-container.m.css.d.ts @@ -1,8 +1,9 @@ export const tabButtons: string; export const tabButton: string; +export const root: string; +export const fixed: string; export const activeTabButton: string; export const tabButtonContent: string; -export const root: string; export const activeTabButtonLabel: string; export const disabledTabButton: string; export const close: string; @@ -14,3 +15,6 @@ export const alignRight: string; export const alignLeft: string; export const tabs: string; export const alignBottom: string; +export const scroller: string; +export const scrollArea: string; +export const scrollContent: string;