Skip to content

Commit

Permalink
Merge pull request #149 from capitec/Add-Tab-component
Browse files Browse the repository at this point in the history
Add Tab component
  • Loading branch information
stefan505 authored Jul 3, 2023
2 parents 633989c + f8caa8a commit 996214e
Show file tree
Hide file tree
Showing 12 changed files with 989 additions and 8 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"calendar": true,
"keyboard": true,
"modal": true,
"tab": true,
"toast": true
}
}
66 changes: 61 additions & 5 deletions custom-elements-manifest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,62 @@ const plugins = {
},
};
}(),
function cssInheritPlugin() {
return {
// Runs for each module
analyzePhase({ ts, node, moduleDoc }) {
switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration:
const className = node.name.getText();

node.jsDoc?.forEach(jsDoc => {
jsDoc.tags?.forEach(tag => {
const tagName = tag.tagName.getText();
if (tagName && (tagName.toLowerCase() === 'cssinherit')) {
const description = tag.comment;

const classDeclaration = moduleDoc.declarations.find(declaration => declaration.name === className);
if (!classDeclaration.cssInherit) {
classDeclaration.cssInherit = [];
}
classDeclaration.cssInherit.push(description);
}
});
});

break;
}
},
// Runs for each module, after analyzing, all information about your module should now be available
moduleLinkPhase({ moduleDoc }) {
// console.log(moduleDoc);
},
// Runs after all modules have been parsed, and after post processing
packageLinkPhase(linkedPackage) {
linkedPackage.customElementsManifest.modules.forEach((m) => {
m.declarations.forEach((d) => {
if (d.cssInherit) {
d.cssInherit.forEach(elementName => {
const module = linkedPackage.customElementsManifest.modules.find((module) =>
module.declarations.find((d) => (d.tagName === elementName && d.customElement) || d.name === elementName));
if (!module) {
return;
}
const declaration = module.declarations.find((d) => (d.tagName === elementName && d.customElement) || d.name === elementName);
if (declaration && declaration.cssProperties) {
if (!d.cssProperties) {
d.cssProperties = [];
}
d.cssProperties = [...d.cssProperties, ...JSON.parse(JSON.stringify(declaration.cssProperties))];
}
})
}
})
})
// console.log(customElementsManifest);
},
};
}(),
function globalAttributesPlugin() {
return {
// Runs for each module
Expand All @@ -80,12 +136,12 @@ const plugins = {
jsDoc.tags?.forEach(tag => {
const tagName = tag.tagName.getText();
if (tagName && (tagName.toLowerCase() === 'globalattr' || tagName.toLowerCase() === 'global_attribute')) {
let attribute = tag.comment.substring(0,tag.comment.indexOf(' - '));
let attribute = tag.comment.substring(0, tag.comment.indexOf(' - '));
let type = '';
if (attribute.includes('}')) {
const split = attribute.split('}');
attribute = split[split.length - 1];
type = split[0].replace('{','');
type = split[0].replace('{', '');
}
const description = tag.comment.substring(tag.comment.indexOf(' - ') + 3);

Expand Down Expand Up @@ -275,16 +331,16 @@ const plugins = {
case ts.SyntaxKind.PropertyDeclaration:
const propertyName = node.name.getText();
const classNode = findParentClass(ts, node);
if (classNode){
if (classNode) {
const className = classNode.name.getText();

node.jsDoc?.forEach(jsDoc => {
jsDoc.tags?.forEach(tag => {
const tagName = tag.tagName.getText();
if (tagName && (tagName.toLowerCase() === 'no_attribute')) {
const value = tag.comment;
const classDeclaration = moduleDoc.declarations.find(declaration => declaration.name === className);

const attributes = classDeclaration.attributes.filter(a => a.name !== propertyName && a.fieldName !== propertyName);
classDeclaration.attributes = attributes;

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"calendar",
"keyboard",
"modal",
"toast"
"toast",
"tab"
],
"exports": {
"./*": "./dist/*/index.js"
Expand Down
232 changes: 232 additions & 0 deletions src/tab/Tab.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { within } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import * as jest from 'jest-mock';
import { html } from 'lit';
import expect from '../utils/ExpectDOM.js';
import { ComponentStoryFormat, querySelectorAsync, getSourceFromLit } from '../utils/StoryUtils.js';

import { Tab } from './Tab.js';
import { TabGroup } from './TabGroup.js';
import { TabHeader } from './TabHeader.js';
import '../label/Label.js';
import './Tab.js';
import './TabGroup.js';

interface Args {
header: string;
active: boolean;
disabled: boolean;
}

export const Basic = {
render: () => html`
<omni-tab-group data-testid="test-tab-group">
<omni-tab header="Tab 1">
<div>Tab 1 Content</div>
</omni-tab>
<omni-tab header="Tab 2">
<div>Tab 2 Content</div>
</omni-tab>
<omni-tab header="Tab 3">
<div>Tab 3 Content</div>
</omni-tab>
</omni-tab-group>
`,
frameworkSources: [
{
framework: 'React',
load: () => `import { OmniTabGroup, OmniTab } from "@capitec/omni-components-react/tab";
import { OmniLabel } from "@capitec/omni-components-react/label";
const App = () =>
<OmniTabGroup>
<OmniTab header="Tab 1">
<OmniLabel label="Label of Tab 1"/>
</OmniTab>
<OmniTab header="Tab 2">
<OmniLabel label="Label of Tab 2"/>
</OmniTab>
<OmniTab header="Tab 3">
<OmniLabel label="Label of Tab 3"/>
</OmniTab>
</OmniTabGroup>;`
}
],
name: 'Basic',
description: () => html`
<div>
This is the recommended use. Headers for each tab is set by the <code>header</code> attribute.
<div>
`,
play: async (context) => {
const tabGroupElement = within(context.canvasElement).getByTestId<TabGroup>('test-tab-group');

// Get the tab bar element
const tabBar = (await querySelectorAsync(tabGroupElement.shadowRoot as ShadowRoot, '.tab-bar')) as HTMLElement;
await expect(tabBar).toBeTruthy();

// Get all the tab headers in the tab bar
const nestedTabHeaders = tabBar.querySelectorAll('omni-tab-header');
const tabHeadersArray = [...nestedTabHeaders];
// Confirm that 3 Tab headers exist.
await expect(tabHeadersArray.length).toBe(3);

// Get the default slot of the Tab element.
const tabsSlotElement = (await querySelectorAsync(tabGroupElement.shadowRoot as ShadowRoot, 'slot:not([name])')) as HTMLSlotElement;
// Get the all the Tab elements in the default slot.
const tabElements = tabsSlotElement.assignedElements();
// Confirm that there is 3 tab elements in the default slot.
await expect(tabElements.length).toBe(3);
}
} as ComponentStoryFormat<Args>;

export const Active = {
render: () => html`
<omni-tab-group data-testid='test-tab-group'>
<omni-tab header="Tab 1">
<omni-label label="Label of Tab 1"></omni-label>
</omni-tab>
<omni-tab header="Tab 2" active>
<omni-label label="Label of Tab 2"></omni-label>
</omni-tab>
<omni-tab header="Tab 3">
<omni-label label="Label of Tab 3"></omni-label>
</omni-tab>
</omni-tab-group>
`,

frameworkSources: [
{
framework: 'Vue',
load: (args) => getSourceFromLit(Active?.render?.(args), undefined, (s) => s.replace(' active', ' :active="true"'))
},
{
framework: 'React',
load: () => `import { OmniTabGroup, OmniTab } from "@capitec/omni-components-react/tab";
import { OmniLabel } from "@capitec/omni-components-react/label";
const App = () =>
<OmniTabGroup>
<OmniTab header="Tab 1">
<OmniLabel label='Label of Tab 1'/>
</OmniTab>
<OmniTab header="Tab 2" active>
<OmniLabel label='Label of Tab 2'/>
</OmniTab>
<OmniTab header="Tab 3">
<OmniLabel label='Label of Tab 3'/>
</OmniTab>
</OmniTabGroup>;`
}
],
args: {},
name: 'Active',
description: () => html`
<div>
Set the <code>active</code> attribute on an <code class="language-html">&lt;omni-tab&gt;</code> to indicate its active. By default, the first slotted one is active.
<div>
`,
play: async (context) => {
const tabGroupElement = within(context.canvasElement).getByTestId<TabGroup>('test-tab-group');

// Get the tab bar element
const tabBar = (await querySelectorAsync(tabGroupElement.shadowRoot as ShadowRoot, '.tab-bar')) as HTMLElement;
await expect(tabBar).toBeTruthy();

// Get all the tab headers in the tab bar
const nestedTabHeaders = tabBar.querySelectorAll('omni-tab-header');
const tabHeadersArray = [...nestedTabHeaders];

//Get the active tab header.
const activeTabHeader = tabHeadersArray.find((c) => c.hasAttribute('data-active'));
await expect(activeTabHeader).toBeTruthy();
// Confirm that the active tab header is the second one in the tab header array
await expect(activeTabHeader).toEqual(tabHeadersArray[1]);
// Click on the first tab header
await userEvent.click(tabHeadersArray[0]);

// Get the default slot for all the Tabs
const tabsSlotElement = (await querySelectorAsync(tabGroupElement.shadowRoot as ShadowRoot, 'slot:not([name])')) as HTMLSlotElement;
// Get the active tab
const tabElement = tabsSlotElement.assignedElements().find((e) => e.hasAttribute('active')) as Tab;
// Get the active tab component slot
const tabElementSlot = (await querySelectorAsync(tabElement.shadowRoot as ShadowRoot, 'slot')) as HTMLSlotElement;
// Get the active tab label element based on the value of the label attribute and confirm that the label exists.
const labelElement = tabElementSlot.assignedElements().find((e) => e.getAttribute('label') === 'Label of Tab 1') as Tab;
await expect(labelElement).toBeTruthy();
}
} as ComponentStoryFormat<Args>;

export const Disabled = {
render: () => html`
<omni-tab-group data-testid="test-tab-group">
<omni-tab header="Tab 1">
<omni-label label="Label of Tab 1"></omni-label>
</omni-tab>
<omni-tab header="Tab 2">
<omni-label label="Label of Tab 2"></omni-label>
</omni-tab>
<omni-tab header="Tab 3" disabled>
<omni-label label="Label of Tab 3"></omni-label>
</omni-tab>
</omni-tab-group>
`,
frameworkSources: [
{
framework: 'Vue',
load: (args) => getSourceFromLit(Disabled?.render?.(args), undefined, (s) => s.replace(' disabled', ' :disabled="true"'))
},
{
framework: 'React',
load: () => `import { OmniTabGroup, OmniTab } from "@capitec/omni-components-react/tab";
import { OmniLabel } from "@capitec/omni-components-react/label";
const App = () =>
<OmniTabGroup>
<OmniTab header="Tab 1">
<OmniLabel label="Label of Tab 1"/>
</OmniTab>
<OmniTab header="Tab 2">
<OmniLabel label="Label of Tab 2"/>
</OmniTab>
<OmniTab header="Tab 3" disabled>
<OmniLabel label="Label of Tab 3"/>
</OmniTab>
</OmniTabGroup>;`
}
],
name: 'Disabled',
description: () => html`
<div>
Set the <code>disabled</code> attribute on an <code class="language-html">&lt;omni-tab&gt;</code> to indicate its disabled.
<div>
`,
args: {},
play: async (context) => {
const tabGroupElement = within(context.canvasElement).getByTestId<TabGroup>('test-tab-group');
const tabSelect = jest.fn();
tabGroupElement.addEventListener('tab-select', tabSelect);

// Get the tab bar element
const tabBar = (await querySelectorAsync(tabGroupElement.shadowRoot as ShadowRoot, '.tab-bar')) as HTMLElement;

// Get all the tabs in the tab bar
const tabHeaders = tabBar.querySelectorAll('omni-tab-header');
const tabsHeadersArray = [...tabHeaders];

//Get the disabled tab.
const disabledTabHeader = tabsHeadersArray.find((c) => c.hasAttribute('data-disabled')) as TabHeader;
// Confirm that the disabled tab the last tab in the tab header array.
await expect(disabledTabHeader).toEqual(tabsHeadersArray[2]);

//Click the disabled tab header twice
await userEvent.click(disabledTabHeader);
await userEvent.click(disabledTabHeader);
// Confirm that the tab select event was emitted zero times.
await expect(tabSelect).toBeCalledTimes(0);
// Click the second tab header
await userEvent.click(tabsHeadersArray[1]);
// Confirm that the tab select event was emitted once.
await expect(tabSelect).toBeCalledTimes(1);
}
} as ComponentStoryFormat<Args>;
Loading

0 comments on commit 996214e

Please sign in to comment.