Skip to content

feat: Tab component #252

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

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions player/react/src/lib/components/index.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import * as progressIndicator from './ui-progress-indicator';
import * as row from './ui-row';
import * as tag from './ui-tag';
import * as searchinput from './ui-search-input';
import * as tabs from './ui-tabs';
import * as text from './ui-text';
import * as textarea from './ui-text-area';
import * as textinput from './ui-text-input';
@@ -56,6 +57,7 @@ export const allComponents = {
row,
searchinput,
tag,
tabs,
text,
textarea,
textinput,
122 changes: 122 additions & 0 deletions player/react/src/lib/components/ui-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from 'react';
import { Tabs, Tab, TabList, TabPanels, TabPanel } from '@carbon/react';
import { CssClasses, SendSignal } from '../types';
import { renderComponents, setItemInState } from '../utils';
import { cx } from 'emotion';
import { commonSlots, slotsDisabled } from '../common-slots';

export interface TabsState {
type: string;
id: string | number;
tabType: string;
isFollowFocused: boolean;
isCacheActive: boolean;
isNavigation: boolean;
items?: [];
selectedTab: number;
cssClasses?: CssClasses[];
codeContext?: {
name: string;
};
}

export interface TabState {
type: string;
id?: string | number;
disabled?: boolean;
label: string;
items?: any[];
cssClasses?: CssClasses[];
codeContext: {
name?: string;
};
}

export const type = 'tabs';

export const signals = ['click'];

export const slots = {
...commonSlots,
...slotsDisabled,
type: 'string',
isFollowFocused: 'boolean',
followFocus: (state: TabsState) => ({
...state,
isFollowFocused: true
}),
deFollowFocus: (state: TabsState) => ({
...state,
isFollowFocused: false
}),
toggleFollowFocus: (state: TabsState) => ({
...state,
isFollowFocused: !state.isFollowFocused
}),
isCacheActive: 'boolean',
cacheActive: (state: TabsState) => ({
...state,
isCacheActive: true
}),
deCacheActive: (state: TabsState) => ({
...state,
isCacheActive: false
}),
toggleCacheActive: (state: TabsState) => ({
...state,
isCacheActive: !state.isCacheActive
}),
isNavigation: 'boolean',
navigation: (state: TabsState) => ({
...state,
isNavigation: true
}),
deNavigation: (state: TabsState) => ({
...state,
isNavigation: false
}),
toggleNavigation: (state: TabsState) => ({
...state,
isNavigation: !state.isNavigation
})
};

export const UITabs = ({ state, setState, setGlobalState, sendSignal }: {
state: TabsState;
setState: (state: any) => void;
setGlobalState: (state: any) => void;
sendSignal: SendSignal;
}) => {
if (state.type !== 'tabs') {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
return <Tabs>
<TabList aria-label='List of tabs' {...(state.tabType !== 'line' ? { contained: true } : {})}>
{
state.items?.map((step: any, index: any) => <Tab
className={cx(step.className, step.cssClasses?.map((cc: any) => cc.id).join(' '))}
onClick={(i: any) => state.selectedTab = i}
key= {index}
disabled={step.disabled}>
{step.labelText}
</Tab>)
}
</TabList>
<TabPanels>
{
state.items?.map((step: any, index: any) => {
const setTabItem = (i: any) => setItemInState(i, step, setState);
return <TabPanel key={index}>
{
step.items?.map((element: any) => {
const setItem = (j: any) => setItemInState(j, element, setTabItem);
return renderComponents(element, setItem, setGlobalState, sendSignal);
})
}
</TabPanel>;
})
}
</TabPanels>
</Tabs>;
};
3 changes: 3 additions & 0 deletions player/react/src/lib/utils.tsx
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ import { UITile } from './components/ui-tile';
import { UITileFold } from './components/ui-tile-fold';
import { UIToggle } from './components/ui-toggle';
import { kebabCase } from 'lodash';
import { UITabs } from './components/ui-tabs';
import { SendSignal } from './types';

export const setItemInState = (item: any, state: any, setState: (state: any) => void) => {
@@ -313,6 +314,8 @@ export const renderComponents = (
case 'radio-tile-group':
return <UIRadioTileGroup key={state.id} state={state} sendSignal={sendSignal} setState={setState} setGlobalState={setGlobalState} />;

case 'tabs':
return <UITabs key={state.id} state={state} sendSignal={sendSignal} setState={setState} setGlobalState={setGlobalState} />;
default:
break;
}
18 changes: 18 additions & 0 deletions sdk/react/src/lib/assets/component-icons/tabs.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
357 changes: 357 additions & 0 deletions sdk/react/src/lib/fragment-components/a-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
import React from 'react';
import {
Tabs,
Tab,
TabList,
Checkbox,
Dropdown,
TextInput,
TabPanel,
TabPanels
} from '@carbon/react';
import { AComponent, ComponentInfo } from './a-component';
import image from '../assets/component-icons/tabs.svg';
import { APlaceholder } from './a-placeholder';
import { cx } from 'emotion';
import {
reactClassNamesFromComponentObj,
angularClassNamesFromComponentObj,
nameStringToVariableString,
updatedState
} from '../helpers/tools';
import { DraggableTileList } from '../helpers/draggable-list';

export const ATabsSettingsUI = ({ selectedComponent, setComponent }: any) => {

const typeItems = [
{ id: 'line', text: 'Inline' },
{ id: 'contained', text: 'Contained' }
];
const updateListItems = (key: string, value: any, index: number) => {
const step = {
...selectedComponent.items[index],
[key]: value
};

setComponent({
...selectedComponent,
items: [
...selectedComponent.items.slice(0, index),
step,
...selectedComponent.items.slice(index + 1)
]
});
};

const template = (item: any, index: number) => {
return <>
<TextInput
id='tab-label'
light
value={item.labelText}
labelText='Tab Label'
onChange={(event: any) => {
updateListItems('labelText', event.currentTarget.value, index);
}} />
<div>
<Checkbox
labelText='Disabled'
id={`disabled-${index}`}
checked={item.disabled}
onChange={(_: any, { checked }: any) => updateListItems('disabled', checked, index)} />
</div>
</>;
};

const updateStepList = (newList: any[]) => {
setComponent({
...selectedComponent,
items: newList
});
};

return <>
<Checkbox
labelText='Follow focus'
id='follow-focus'
checked={selectedComponent.isFollowFocused}
onChange={(_: any, { checked }: any) => setComponent({
...selectedComponent,
isFollowFocused: checked
})} />
<Checkbox
labelText='Cache active'
id='cache-active'
checked={selectedComponent.isCacheActive}
onChange={(_: any, { checked }: any) => setComponent({
...selectedComponent,
isCacheActive: checked
})} />
<Checkbox
labelText='Navigation'
id='navigation'
checked={selectedComponent.isNavigation}
onChange={(_: any, { checked }: any) => setComponent({
...selectedComponent,
isNavigation: checked
})} />
<Dropdown
id='type-dropdown'
label=''
titleText='Tab type'
items={typeItems}
selectedItem={typeItems.find(item => item.id === selectedComponent.tabType)}
itemToString={(item: any) => (item ? item.text : '')}
onChange={(event: any) => setComponent({
...selectedComponent,
tabType: event.selectedItem.id
})} />
<DraggableTileList
dataList={[...selectedComponent.items]}
setDataList={updateStepList}
updateItem={updateListItems}
defaultObject={{
type: 'tab',
labelText: 'New tab',
items: []
}}
template={template} />
</>;
};

export const ATabsCodeUI = ({ selectedComponent, setComponent }: any) => <TextInput
id='input-name'
value={selectedComponent.codeContext?.name}
labelText='Input name'
onChange={(event: any) => {
setComponent({
...selectedComponent,
codeContext: {
...selectedComponent.codeContext,
name: event.currentTarget.value
}
});
}}
/>;

export const ATabs = ({
children,
componentObj,
fragment,
setFragment,
...rest
}: any) => {
return (
<AComponent
fragment={fragment}
setFragment={setFragment}
componentObj={componentObj}
{...rest}>
<Tabs className={componentObj.cssClasses?.map((cc: any) => cc.id).join(' ')}>
<>
<TabList aria-label='List of tabs'
{...(componentObj.tabType !== 'line' ? { contained: true } : {})}>
{
componentObj.items.map((step: any, index: number) => <Tab
onClick= {() => componentObj.selectedTab = index}
className={cx(step.className, componentObj.cssClasses?.map((cc: any) => cc.id).join(' '))}
disabled={step.disabled}
key={index}>
{step.labelText}
</Tab>)
}
</TabList>
<TabPanels>
{
componentObj.items.map((step: any, index: number) => {
return <TabPanel key={index} onDrop={(event: any) => {
event.stopPropagation();
event.preventDefault();
const dragObj = JSON.parse(event.dataTransfer.getData('drag-object'));
// update the tab content in the item list for the selected tab
const items = componentObj.items.map((item: any, index: any) => {
if (index === componentObj.selectedTab) {
return {
...step,
items: [
...step.items,
dragObj.component
]
};
}
return item;
});
setFragment({
...fragment,
data: updatedState(fragment.data, {
type: 'update',
component: {
...componentObj,
items
}
})
}, false);
}}>
{
step.items && step.items.length > 0
? children.filter((child: any, index: any) => index === componentObj.selectedTab)
: <APlaceholder componentObj={step} select={rest.select} />
}
</TabPanel>;
}
)
}
</TabPanels>
</>
</Tabs>
</AComponent>
);
};

export const componentInfo: ComponentInfo = {
component: ATabs,
settingsUI: ATabsSettingsUI,
codeUI: ATabsCodeUI,
render: ({ componentObj, select, selected, remove, renderComponents, outline, fragment, setFragment }) => <ATabs
componentObj={componentObj}
select={select}
remove={remove}
fragment={fragment}
setFragment={setFragment}
selected={selected}>
{
// render the child components within each tab.
componentObj.items.map((tab: any) => {
if (tab.items && tab.items.length > 0) {
return tab.items.map((item: any) => {
return renderComponents(item, outline);
});
}
})
}
</ATabs>,
keywords: ['tabs', 'tab'],
name: 'Tabs',
type: 'tabs',
defaultComponentObj: {
type: 'tabs',
items: [
{
type: 'tab',
labelText: 'Tab 1',
items: []
}
]
},
image,
codeExport: {
angular: {
latest: {
inputs: ({ json }) => `
@Input() ${nameStringToVariableString(json.codeContext?.name)}FollowFocus = ${
json.isFollowFocused ? json.isFollowFocused : false};
@Input() ${nameStringToVariableString(json.codeContext?.name)}CacheActive = ${json.isCacheActive ? json.isCacheActive : false};
@Input() ${nameStringToVariableString(json.codeContext?.name)}isNavigation = ${json.isNavigation ? json.isNavigation : true};
@Input() ${nameStringToVariableString(json.codeContext?.name)}TabType: any = "${json.tabType ? json.tabType : 'contained'}";`,
outputs: () => '',
imports: ['TabsModule'],
code: ({ json, fragments, jsonToTemplate, customComponentsCollections }) => {
return `<cds-tabs
[type]="${nameStringToVariableString(json.codeContext?.name)}TabType"
[cacheActive]="${nameStringToVariableString(json.codeContext?.name)}CacheActive"
[followFocus]="${nameStringToVariableString(json.codeContext?.name)}FollowFocus"
[isNavigation]="${nameStringToVariableString(json.codeContext?.name)}isNavigation"
${angularClassNamesFromComponentObj(json)}>
${json.items.map((step: any) => `<cds-tab
heading="${step.labelText}"
[disabled]=${step.disabled}>
${step.items && step.items.length > 0
? step.items.map((element: any) =>
jsonToTemplate(element, fragments, customComponentsCollections)).join('\n') : ''}
</cds-tab>`
).join('\n')}
</cds-tabs>`;
}
},
v10: {
inputs: ({ json }) => `
@Input() ${nameStringToVariableString(json.codeContext?.name)}FollowFocus = ${json.isFollowFocused ? json.isFollowFocused : false};
@Input() ${nameStringToVariableString(json.codeContext?.name)}CacheActive = ${json.isCacheActive ? json.isCacheActive : false};
@Input() ${nameStringToVariableString(json.codeContext?.name)}isNavigation = ${json.isNavigation ? json.isNavigation : true};
@Input() ${nameStringToVariableString(json.codeContext?.name)}TabType: any = "${json.tabType ? json.tabType : 'contained'}";`,
outputs: () => '',
imports: ['TabsModule'],
code: ({ json, fragments, jsonToTemplate, customComponentsCollections }) => {
return `<ibm-tabs
[type]="${nameStringToVariableString(json.codeContext?.name)}TabType"
[cacheActive]="${nameStringToVariableString(json.codeContext?.name)}CacheActive"
[followFocus]="${nameStringToVariableString(json.codeContext?.name)}FollowFocus"
[isNavigation]="${nameStringToVariableString(json.codeContext?.name)}isNavigation"
${angularClassNamesFromComponentObj(json)}>
${json.items.map((step: any) => `<ibm-tab
heading="${step.labelText}"
[disabled]=${step.disabled}>
${step.items && step.items.length > 0
? step.items.map((element: any) =>
jsonToTemplate(element, fragments, customComponentsCollections)).join('\n') : ''}
</ibm-tab>`
).join('\n')}
</ibm-tabs>`;
}
}
},
react: {
latest: {
imports: ['Tabs', 'Tab', 'TabList', 'TabPanels', 'TabPanel'],
code: ({ json, fragments, jsonToTemplate, customComponentsCollections }) => {
return `<Tabs
${reactClassNamesFromComponentObj(json)}>
<TabList aria-label="List of tabs" ${json.tabType === 'line'? '' : 'contained'}>
${json.items.map((step: any, index: any) => `<Tab
onClick={(index) => handleInputChange({
target: {
selectedTab: ${index}
}
})}
key= {${index}}
disabled={${step.disabled}}>
${step.labelText}
</Tab>`
).join('\n')}
</TabList>
<TabPanels>
${json.items.map((step: any) => `<TabPanel>
${step.items && step.items.length > 0
? step.items.map((element: any) =>
jsonToTemplate(element, fragments, customComponentsCollections)).join('\n') : ''}
</TabPanel>`
).join('\n')}
</TabPanels>
</Tabs>`;
}
},
v10: {
imports: ['Tabs', 'Tab'],
code: ({ json, fragments, jsonToTemplate, customComponentsCollections }) => {
return `<Tabs
${reactClassNamesFromComponentObj(json)}>
${json.items.map((step: any, index: any) => `<Tab
onClick={(index) => handleInputChange({
target: {
selectedTab: ${index}
}
})}
key= {${index}}
disabled={${step.disabled}}
label="${step.labelText}">
${step.items && step.items.length > 0
? step.items.map((element: any) =>
jsonToTemplate(element, fragments, customComponentsCollections)).join('\n') : ''}
</Tab>`
).join('\n')}
</Tabs>`;
}
}
}
}
};
4 changes: 3 additions & 1 deletion sdk/react/src/lib/fragment-components/index.ts
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ import * as link from './a-link';
import * as loading from './a-loading';
import * as inlineLoading from './a-inline-loading';
import * as overflowMenu from './a-overflow-menu';
import * as tabs from './a-tabs';
// Tiles
import * as tile from './tiles/a-tile';
import * as toggle from './a-toggle';
@@ -63,7 +64,7 @@ export { ATextInput, ATextInputSettingsUI, ATextInputCodeUI } from './a-text-inp
export { AOverflowMenu, AOverflowMenuCodeUI, AOverflowMenuSettingsUI } from './a-overflow-menu';
export { ARadio, ARadioSettingsUI, ARadioCodeUI } from './a-radio';
export { ARadioGroup, ARadioGroupSettingsUI, ARadioGroupCodeUI } from './a-radio-group';

export { ATabs, ATabsSettingsUI, ATabsCodeUI } from './a-tabs';
// Tiles
export { ATile, ATileCodeUI, ATileSettingsUI } from './tiles/a-tile';
export { AToggle, AToggleSettingsUI, AToggleCodeUI } from './a-toggle';
@@ -76,6 +77,7 @@ export { ARadioTile, ARadioTileCodeUI, ARadioTileSettingsUI } from './tiles/a-ra
export { ARadioTileGroup, ARadioTileGroupCodeUI, ARadioTileGroupSettingsUI } from './tiles/a-radio-tile-group';

export const allComponents = {
tabs,
accordion,
accordionitem,
button,

Unchanged files with check annotations Beta

if (setExternalSettings) {
setExternalSettings(settings);
}
}, [settings]);

Check warning on line 211 in sdk/react/src/lib/components/settings-context-pane.tsx

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'setExternalSettings'. Either include it or remove the dependency array. If 'setExternalSettings' changes too often, find the parent component that defines it and wrap that definition in useCallback
return (
<div className='context-pane-content'>