Skip to content

Commit

Permalink
feat(forms): migrate to DS Tabs + CollapsibleFieldset (#4928)
Browse files Browse the repository at this point in the history
  • Loading branch information
sgendre authored Nov 28, 2023
1 parent cb5cb9a commit ea026ec
Show file tree
Hide file tree
Showing 18 changed files with 372 additions and 213 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-terms-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@talend/react-forms': minor
---

Use DS tabs in UIForm
5 changes: 5 additions & 0 deletions .changeset/few-months-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@talend/design-system': minor
---

Error state for tabs
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,24 @@
margin-right: tokens.$coral-spacing-s;
}
}

&_error {
color: tokens.$coral-color-danger-text;

&[aria-selected='true'] {
color: tokens.$coral-color-danger-text;
}
}

&_error::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
border-top: tokens.$coral-border-m-solid tokens.$coral-color-danger-text;
opacity: 0;
transition: tokens.$coral-transition-fast;
transform: translateY(100%);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type TabPropTypes = {
icon?: IconNameWithSize<'S'>;
tag?: string | number;
tooltip?: string;
error?: boolean;
};

export function Tab(props: TabPropTypes) {
Expand All @@ -36,7 +37,10 @@ export function Tab(props: TabPropTypes) {
<button
role="tab"
aria-selected={props['aria-controls'] === context?.value}
className={classNames(style.tab, { [style.tab_large]: context?.size === 'L' })}
className={classNames(style.tab, {
[style.tab_large]: context?.size === 'L',
[style.tab_error]: props.error === true,
})}
onClick={e => context?.onChange(e, props['aria-controls'])}
disabled={props.disabled}
type="button"
Expand Down
9 changes: 3 additions & 6 deletions packages/design-system/src/components/Tabs/variants/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,9 @@ export function Tabs(props: TabsProps) {
tabProps['aria-controls'] = ids[index];
tabProps.title = tab.tabTitle;
} else if (typeof tab.tabTitle === 'object') {
tabProps['aria-controls'] = tab.tabTitle.id || ids[index];
tabProps.title = tab.tabTitle.title;
tabProps.icon = tab.tabTitle.icon;
tabProps.tag = tab.tabTitle.tag;
tabProps.tooltip = tab.tabTitle.tooltip;
tabProps.disabled = tab.tabTitle.disabled;
const { id, ...rest } = tab.tabTitle;
tabProps['aria-controls'] = id || ids[index];
Object.assign(tabProps, rest);
}
return <Tab key={index} {...(tabProps as TabPropTypes)} />;
})}
Expand Down
22 changes: 22 additions & 0 deletions packages/design-system/src/stories/navigation/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ export const Styles = () => (
</StackHorizontal>
);

export const TabsWithError = () => (
<Tabs
tabs={[
{
tabTitle: {
icon: 'user',
title: 'User',
error: true,
},
tabContent: <h2>Users tab content</h2>,
},
{
tabTitle: {
icon: 'calendar',
title: 'Calendar',
},
tabContent: <h2>Calendar tab content</h2>,
},
]}
/>
);

export const TabsWithIcon = () => (
<Tabs.Container defaultActiveKey="profile">
<Tabs.List>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export default class ArrayWidget extends Component {
<Widget
{...this.props}
{...extraProps}
index={index}
disabled={this.props.schema.disabled}
id={this.props.id && `${this.props.id}-${index}`}
schema={getArrayElementSchema(this.props.schema, index)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useCallback } from 'react';

import PropTypes from 'prop-types';
import classNames from 'classnames';
import { InlineMessageInformation } from '@talend/design-system';
import CollapsiblePanel from '@talend/react-components/lib/CollapsiblePanel';
import { InlineMessageInformation, CollapsiblePanel } from '@talend/design-system';
import get from 'lodash/get';
import Widget from '../../Widget';
import { generateDescriptionId } from '../../Message/generateId';
Expand Down Expand Up @@ -72,11 +73,18 @@ export function defaultTitle(formData, schema, options) {
* @param {function} title the function called by the component to compute the title
* @return {function} CollapsibleFieldset react component
*/
// eslint-disable-next-line @typescript-eslint/default-param-last
export default function createCollapsibleFieldset(title = defaultTitle) {
function CollapsibleFieldset(props) {
function toggle(event) {
event.stopPropagation();
event.preventDefault();
const { id, schema, value, actions, index, ...restProps } = props;
const { items, managed } = schema;

function onToggleClick(event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}

const payload = {
schema: props.schema,
value: {
Expand All @@ -87,22 +95,31 @@ export default function createCollapsibleFieldset(title = defaultTitle) {
props.onChange(event, payload);
}

const { id, schema, value, actions, ...restProps } = props;
const { items } = schema;
const displayAction = actions.map(action => ({
...action,
displayMode: CollapsiblePanel.displayModes.TYPE_ACTION,
}));
const getAction = useCallback(() => {
if (!actions || actions.length === 0 || actions[0] === undefined) {
return undefined;
}

const action = actions[0];

return {
...action,
tooltip: action.tooltip || action.label,
callback: action.onClick,
};
}, [actions]);

return (
<fieldset
className={classNames('form-group', theme['collapsible-panel'], 'collapsible-panel')}
>
<CollapsiblePanel
id={`${id}`}
header={[{ label: title(value, schema) }, displayAction]}
onToggle={toggle}
title={title(value, schema)}
onToggleExpanded={onToggleClick}
index={index}
managed={!!managed}
expanded={!value.isClosed}
action={getAction()}
>
{schema.description ? (
<InlineMessageInformation
Expand All @@ -115,8 +132,8 @@ export default function createCollapsibleFieldset(title = defaultTitle) {
) : (
''
)}
{items.map((itemSchema, index) => (
<Widget {...restProps} id={id} key={index} schema={itemSchema} value={value} />
{items.map((itemSchema, idx) => (
<Widget {...restProps} id={id} key={`${id}-${idx}`} schema={itemSchema} value={value} />
))}
</CollapsiblePanel>
</fieldset>
Expand All @@ -132,9 +149,11 @@ export default function createCollapsibleFieldset(title = defaultTitle) {
if (process.env.NODE_ENV !== 'production') {
CollapsibleFieldset.propTypes = {
id: PropTypes.string,
index: PropTypes.number,
onChange: PropTypes.func.isRequired,
schema: PropTypes.shape({
items: PropTypes.array.isRequired,
managed: PropTypes.bool,
description: PropTypes.string,
}).isRequired,
value: PropTypes.object,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe('CollapsibleFieldset', () => {
<CollapsibleFieldset {...props} value={{ ...value, isClosed: true }} />
</WidgetContext.Provider>,
);
expect(screen.getByRole('tab')).toHaveTextContent('Jimmy, Somsanith');
expect(screen.getByText('Jimmy, Somsanith')).toBeInTheDocument();
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false');
});
it('should render a custom title', () => {
Expand All @@ -101,7 +101,7 @@ describe('CollapsibleFieldset', () => {
<CollapsibleFieldset {...props} />
</WidgetContext.Provider>,
);
expect(screen.getByRole('tab')).toHaveTextContent('Basic: Jimmy Somsanith');
expect(screen.getByText('Basic: Jimmy Somsanith')).toBeInTheDocument();
});
it('should render without value', () => {
const CollapsibleFieldset = createCollapsibleFieldset();
Expand All @@ -110,42 +110,48 @@ describe('CollapsibleFieldset', () => {
<CollapsibleFieldset {...props} value={{}} />
</WidgetContext.Provider>,
);
expect(screen.getByRole('tab')).toHaveTextContent('Basic');
expect(screen.getByText('Basic')).toBeInTheDocument();
});

it('should toggle', async () => {
// given
const CollapsibleFieldset = createCollapsibleFieldset();

const extendedSchema = {
...props.schema,
managed: true,
};

render(
<WidgetContext.Provider value={widgets}>
<CollapsibleFieldset {...props} value={{ ...value, isClosed: true }} />
<CollapsibleFieldset
{...props}
value={{ ...value, isClosed: true }}
schema={extendedSchema}
index={0}
/>
</WidgetContext.Provider>,
);
// when
await userEvent.click(screen.getByRole('button'));

// then
expect(props.onChange).toBeCalledWith(expect.anything(), {
schema,
schema: extendedSchema,
value: { ...value, isClosed: false },
});
});

it('should render Actions component if actions are provided', () => {
const CollapsibleFieldset = createCollapsibleFieldset();
const actions = [
{ id: 'action1', label: 'Action1', onClick: jest.fn() },
{ id: 'action2', label: 'Action 2', onClick: jest.fn() },
];
const actions = [{ id: 'action1', label: 'Action1', onClick: jest.fn(), icon: 'talend-trash' }];

render(
<WidgetContext.Provider value={widgets}>
<CollapsibleFieldset {...props} actions={actions} />
</WidgetContext.Provider>,
);
expect(screen.getByRole('button', { name: 'Action1' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Action 2' })).toBeVisible();
});

it('should not render Actions component if actions are not provided', () => {
Expand Down
Loading

0 comments on commit ea026ec

Please sign in to comment.