Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1732 - non-fixed style tabs with scrolling #1747

Merged
merged 8 commits into from
Apr 27, 2021
6 changes: 6 additions & 0 deletions src/examples/src/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand Down
35 changes: 35 additions & 0 deletions src/examples/src/widgets/tab-container/VariableWidth.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Example>
<TabContainer tabs={tabs} fixed={false}>
<div key="tab0">
<Button
onClick={() => {
console.log('click');
}}
>
Button
</Button>
</div>
<div key="tab1">Hello Tab Two</div>
<div key="tab2">Hello Tab Three</div>
<div key="tab3">Hello Tab Four</div>
</TabContainer>
</Example>
);
});
48 changes: 40 additions & 8 deletions src/tab-container/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<TabContainerICache>(),
theme
theme,
offscreen
}).properties<TabContainerProperties>();

export const TabContainer = factory(function TabContainer({
children,
id,
middleware: { focus, i18n, icache, theme },
middleware: { focus, i18n, icache, theme, offscreen },
properties
}) {
const {
Expand All @@ -59,7 +64,8 @@ export const TabContainer = factory(function TabContainer({
onClose,
theme: themeProp,
classes,
variant
variant,
fixed = true
} = properties();
let { activeIndex } = properties();

Expand Down Expand Up @@ -177,10 +183,31 @@ export const TabContainer = factory(function TabContainer({
);
};

const content = [
<div key="buttons" classes={themeCss.tabButtons}>
{tabs.map(renderTab)}
</div>,
const renderedTabs = tabs.map(renderTab);

const horizontalScrollbarHeight = icache.getOrSet('horizontalScrollbarHeight', () => {
return offscreen(
() => <div classes={[themeCss.scrollTest]} />,
(node) => node.offsetHeight - node.clientHeight
);
});

let content = [
fixed ? (
<div key="buttons" classes={themeCss.tabButtons}>
{...renderedTabs}
</div>
) : (
<div
key="scrollArea"
classes={[themeCss.scrollArea, themeCss.scroll]}
styles={{ marginBottom: `${horizontalScrollbarHeight}px` }}
>
<div key="buttons" classes={[themeCss.tabButtons, themeCss.scrollContent]}>
{...renderedTabs}
</div>
</div>
),
<div key="tabs" classes={themeCss.tabs}>
{children().map((child, index) => {
const disabled = tabs[index].disabled;
Expand Down Expand Up @@ -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}
Expand Down
100 changes: 95 additions & 5 deletions src/tab-container/tests/TabContainer.spec.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 };
Expand All @@ -41,19 +42,37 @@ const baseTemplate = assertionTemplate(() => (
<div
key="root"
aria-orientation={'horizontal'}
classes={[undefined, null, css.root]}
classes={[undefined, null, css.root, css.fixed]}
role="tablist"
>
<div key="buttons" classes={css.tabButtons} />
<div key="tabs" classes={css.tabs} />
</div>
));

const scrollableBaseTemplate = assertionTemplate(() => (
<div
key="root"
aria-orientation={'horizontal'}
classes={[undefined, null, css.root, css.scroller]}
role="tablist"
>
<div
key="scrollArea"
classes={[css.scrollArea, css.scroll]}
styles={{ marginBottom: '5px' }}
>
<div key="buttons" classes={[css.tabButtons, css.scrollContent]} />
</div>
<div key="tabs" classes={css.tabs} />
</div>
));

const reverseOrientationTemplate = assertionTemplate(() => (
<div
key="root"
aria-orientation={'vertical'}
classes={[undefined, css.alignRight, css.root]}
classes={[undefined, css.alignRight, css.root, css.fixed]}
role="tablist"
>
<div key="tabs" classes={css.tabs} />
Expand Down Expand Up @@ -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(
() => (
<TabContainer tabs={tabs} fixed={false}>
<div>tab0</div>
<div>tab1</div>
</TabContainer>
),
{
middleware: [[offscreen, offscreenMock]]
}
);

const template = scrollableBaseTemplate
.setChildren('@buttons', () => [
<div {...tabButtonProperties}>
<span
key="tabButtonContent"
classes={[css.tabButtonContent, css.activeTabButtonLabel]}
>
tab0
<span classes={[css.indicator, css.indicatorActive]}>
<span classes={css.indicatorContent} />
</span>
</span>
</div>,
<div
{...tabButtonProperties}
classes={[css.tabButton, null, null, null]}
aria-selected="false"
aria-controls="test-tab-1"
tabIndex={-1}
key="1-tabbutton"
>
<span key="tabButtonContent" classes={[css.tabButtonContent, false]}>
tab1
<span classes={[css.indicator, false]}>
<span classes={css.indicatorContent} />
</span>
</span>
</div>
])
.setChildren('@tabs', () => [
<div classes={css.tab} hidden={false}>
<div>tab0</div>
</div>,
<div classes={undefined} hidden={true}>
<div>tab1</div>
</div>
]);

h.expect(template);

assert.deepEqual(offscreenRenderResult, <div classes={[css.scrollTest]} />);
}
}
});
46 changes: 46 additions & 0 deletions src/theme/default/tab-container.m.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/theme/default/tab-container.m.css.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading