Skip to content

Commit 076d962

Browse files
authored
feat(faceted-search/badge): Add BadgeMenu for single selection (#4891)
1 parent f0c98a7 commit 076d962

18 files changed

+558
-12
lines changed

.changeset/small-sheep-wink.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@talend/react-faceted-search': minor
3+
'@talend/design-system': minor
4+
'@talend/ui-storybook': minor
5+
---
6+
7+
Add BadgeMenu to faceted search for single selection

packages/design-system/src/components/Dropdown/Dropdown.cy.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ const WithIcons = () => (
2222
onClick: () => console.log('fou'),
2323
type: 'button',
2424
},
25+
{
26+
label: 'Button with checked state',
27+
onClick: () => console.log('fou'),
28+
type: 'button',
29+
checked: true,
30+
},
2531
]}
2632
>
2733
<ButtonTertiary isDropdown onClick={() => {}}>
@@ -81,6 +87,7 @@ context('<Dropdown />', () => {
8187
cy.findByTestId('dropdown.menu').should('be.visible');
8288
cy.findByTestId('dropdown.menuitem.Link with icon-0').should('be.visible');
8389
cy.findByTestId('dropdown.menuitem.Button with icon-1').should('be.visible');
90+
cy.findByTestId('dropdown.menuItem.Button.checked').should('be.visible');
8491
cy.get('body').click(0, 0);
8592
cy.findByTestId('dropdown.menu').should('not.be.visible');
8693
});

packages/design-system/src/components/Dropdown/Dropdown.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type DropdownButtonType = Omit<ClickableProps, 'children' | 'as'> & {
1515
onClick: () => void;
1616
icon?: DeprecatedIconNames;
1717
type: 'button';
18+
checked?: boolean;
1819
} & DataAttributes;
1920

2021
type DropdownLinkType = Omit<LinkableType, 'children'> & {

packages/design-system/src/components/Dropdown/Primitive/DropdownButton.tsx

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
import { forwardRef, Ref } from 'react';
2+
import classNames from 'classnames';
23
import { MenuItem, MenuItemProps } from 'reakit';
34
// eslint-disable-next-line @talend/import-depth
45
import { IconNameWithSize } from '@talend/icons/dist/typeUtils';
56

67
import { DeprecatedIconNames } from '../../../types';
78
import Clickable, { ClickableProps } from '../../Clickable';
9+
import { SizedIcon } from '../../Icon';
810
import { getIconWithDeprecatedSupport } from '../../Icon/DeprecatedIconHelper';
911

1012
import styles from './DropdownEntry.module.scss';
1113

1214
export type DropdownButtonType = Omit<ClickableProps, 'as'> &
13-
MenuItemProps & { icon?: DeprecatedIconNames | IconNameWithSize<'M'> };
15+
MenuItemProps & { icon?: DeprecatedIconNames | IconNameWithSize<'M'>; checked?: boolean };
1416

1517
const DropdownButton = forwardRef(
16-
({ children, icon, ...props }: DropdownButtonType, ref: Ref<HTMLButtonElement>) => {
18+
({ children, icon, checked, ...props }: DropdownButtonType, ref: Ref<HTMLButtonElement>) => {
1719
return (
18-
<MenuItem {...props} as={Clickable} className={styles.dropdownEntry} ref={ref}>
20+
<MenuItem
21+
{...props}
22+
as={Clickable}
23+
className={styles.dropdownEntry}
24+
ref={ref}
25+
aria-selected={checked}
26+
>
1927
{icon && (
2028
<span className={styles.buttonIcon}>
2129
{getIconWithDeprecatedSupport({
@@ -25,7 +33,15 @@ const DropdownButton = forwardRef(
2533
})}
2634
</span>
2735
)}
28-
<span className={styles.buttonContent}>{children}</span>
36+
<span className={classNames(styles.buttonContent, { checked })}>{children}</span>
37+
{checked && (
38+
<SizedIcon
39+
name="check"
40+
size="M"
41+
data-test="dropdown.menuItem.Button.checked"
42+
data-testid="dropdown.menuItem.Button.checked"
43+
/>
44+
)}
2945
</MenuItem>
3046
);
3147
},

packages/design-system/src/components/Dropdown/Primitive/DropdownEntry.module.scss

+7-4
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,25 @@ $icon-alignment-offset-m: -0.3rem;
44

55
.dropdownEntry {
66
background: tokens.$coral-color-accent-background-weak;
7-
display: block;
7+
display: flex;
8+
align-items: center;
89
width: 100%;
910
min-width: 0;
1011
color: tokens.$coral-color-neutral-text;
1112
font: tokens.$coral-paragraph-m;
1213
transition: tokens.$coral-transition-fast;
1314
padding: tokens.$coral-spacing-xxs tokens.$coral-spacing-s;
14-
max-width: tokens.$coral-sizing-maximal;
1515
text-align: start;
1616

1717
> span {
1818
min-width: 0;
19+
20+
&:global(.checked) {
21+
font-weight: 600;
22+
}
1923
}
2024

2125
.buttonIcon {
22-
height: tokens.$coral-sizing-xxxs;
23-
width: tokens.$coral-sizing-xxxs;
2426
margin-right: tokens.$coral-spacing-xxs;
2527
flex-shrink: 0;
2628
position: relative;
@@ -29,6 +31,7 @@ $icon-alignment-offset-m: -0.3rem;
2931

3032
.buttonContent {
3133
min-width: 0;
34+
flex: 1;
3235
}
3336

3437
&:hover,
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import Dropdown from './Dropdown';
2+
import DropdownButton from './Primitive/DropdownButton';
23

34
export default Dropdown;
5+
6+
export { DropdownButton };

packages/design-system/src/components/Linkable/LinkableStyles.module.scss

-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ $icon-alignment-offset-s: 0.1rem;
1010
.link__icon {
1111
position: relative;
1212
bottom: $icon-alignment-offset-m;
13-
height: tokens.$coral-sizing-xxxs;
14-
width: tokens.$coral-sizing-xxxs;
1513
margin-right: tokens.$coral-spacing-xxs;
1614
flex-shrink: 0;
1715
}

packages/design-system/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Card from './components/WIP/Card';
1919
import Combobox from './components/WIP/Combobox';
2020
import Divider from './components/Divider';
2121
import { FloatingDrawer } from './components/WIP/Drawer';
22-
import Dropdown from './components/Dropdown';
22+
import Dropdown, { DropdownButton } from './components/Dropdown';
2323
import EmptyState, {
2424
EmptyStateLarge,
2525
EmptyStateMedium,
@@ -122,6 +122,7 @@ export {
122122
Divider,
123123
FloatingDrawer,
124124
Dropdown,
125+
DropdownButton,
125126
EmptyState,
126127
EmptyStateMedium,
127128
EmptyStateSmall,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { useMemo } from 'react';
2+
import { isEmpty } from 'lodash';
3+
import PropTypes from 'prop-types';
4+
import Badge from '@talend/react-components/lib/Badge';
5+
6+
import { BadgeFaceted } from '../BadgeFaceted';
7+
import { BadgeMenuForm } from './BadgeMenuForm.component';
8+
import { operatorPropTypes, operatorsPropTypes } from '../../facetedSearch.propTypes';
9+
10+
const getSelectBadgeLabel = (value, t) => {
11+
const labelAll = t('FACETED_SEARCH_VALUE_ALL', { defaultValue: 'All' });
12+
if (!isEmpty(value)) {
13+
return value.label;
14+
}
15+
return labelAll;
16+
};
17+
18+
// eslint-disable-next-line import/prefer-default-export
19+
export const BadgeMenu = ({
20+
id,
21+
readOnly,
22+
removable,
23+
label,
24+
initialOperatorOpened,
25+
initialValueOpened,
26+
operator,
27+
operators,
28+
size,
29+
value,
30+
values,
31+
displayType,
32+
filterBarPlaceholder,
33+
t,
34+
...rest
35+
}) => {
36+
const currentOperators = useMemo(() => operators, [operators]);
37+
const currentOperator = operator || (currentOperators && currentOperators[0]);
38+
const badgeMenuId = `${id}-badge-menu`;
39+
const badgeLabel = useMemo(() => getSelectBadgeLabel(value, t), [value, t]);
40+
return (
41+
<BadgeFaceted
42+
badgeId={id}
43+
displayType={displayType}
44+
id={badgeMenuId}
45+
initialOperatorOpened={initialOperatorOpened}
46+
initialValueOpened={initialValueOpened}
47+
labelCategory={label}
48+
labelValue={badgeLabel}
49+
operator={currentOperator}
50+
operators={currentOperators}
51+
readOnly={readOnly}
52+
removable={removable}
53+
size={size}
54+
t={t}
55+
value={value || {}}
56+
>
57+
{({ onSubmitBadge, onChangeValue, badgeValue }) => (
58+
<BadgeMenuForm
59+
id={badgeMenuId}
60+
onChange={onChangeValue}
61+
onSubmit={onSubmitBadge}
62+
value={badgeValue}
63+
values={values}
64+
filterBarPlaceholder={filterBarPlaceholder}
65+
t={t}
66+
{...rest}
67+
/>
68+
)}
69+
</BadgeFaceted>
70+
);
71+
};
72+
73+
BadgeMenu.propTypes = {
74+
label: PropTypes.string.isRequired,
75+
id: PropTypes.string.isRequired,
76+
initialOperatorOpened: PropTypes.bool,
77+
initialValueOpened: PropTypes.bool,
78+
operator: operatorPropTypes,
79+
operators: operatorsPropTypes,
80+
size: PropTypes.oneOf(Object.values(Badge.SIZES)),
81+
value: PropTypes.oneOfType([
82+
PropTypes.string,
83+
PropTypes.shape({
84+
checked: PropTypes.bool,
85+
id: PropTypes.string.isRequired,
86+
label: PropTypes.string.isRequired,
87+
}),
88+
]),
89+
readOnly: PropTypes.bool,
90+
removable: PropTypes.bool,
91+
values: PropTypes.array,
92+
t: PropTypes.func.isRequired,
93+
displayType: PropTypes.oneOf(Object.values(Badge.TYPES)),
94+
filterBarPlaceholder: PropTypes.string,
95+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { render } from '@testing-library/react';
2+
import { BadgeFacetedProvider } from '../../context/badgeFaceted.context';
3+
4+
import { BadgeMenu } from './BadgeMenu.component';
5+
import getDefaultT from '../../../translate';
6+
7+
const t = getDefaultT();
8+
9+
const operator = {
10+
label: 'My Operator',
11+
name: 'my-operator',
12+
};
13+
14+
const badgeFacetedContextValue = {
15+
onDeleteBadge: jest.fn(),
16+
onHideOperator: jest.fn(),
17+
onSubmitBadge: jest.fn(),
18+
};
19+
20+
const BadgeWithContext = props => (
21+
<BadgeFacetedProvider value={badgeFacetedContextValue}>
22+
<BadgeMenu {...props} />
23+
</BadgeFacetedProvider>
24+
);
25+
26+
describe('BadgeMenu', () => {
27+
it('should return "All" when there is no value', () => {
28+
// Given
29+
const props = {
30+
id: 'myId',
31+
label: 'My Label',
32+
operator,
33+
operators: ['equals'],
34+
t,
35+
};
36+
// When
37+
render(<BadgeWithContext {...props} />);
38+
// Then
39+
expect(document.querySelectorAll('span')[2]).toHaveTextContent('All');
40+
});
41+
it('should return "All" when value is empty', () => {
42+
// Given
43+
const props = {
44+
id: 'myId',
45+
label: 'My Label',
46+
operator,
47+
operators: ['equals'],
48+
t,
49+
value: {},
50+
};
51+
// When
52+
render(<BadgeWithContext {...props} />);
53+
// Then
54+
expect(document.querySelectorAll('span')[2]).toHaveTextContent('All');
55+
});
56+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
@use '~@talend/design-tokens/lib/tokens';
2+
3+
$popover-screen-width: tokens.$coral-sizing-maximal;
4+
5+
.fs-badge-menu-form {
6+
:global(.tc-tooltip-body) {
7+
margin: 0;
8+
overflow-y: auto;
9+
max-width: auto;
10+
width: $popover-screen-width;
11+
}
12+
13+
&-header:not(:empty) {
14+
height: auto;
15+
flex-direction: column;
16+
align-items: stretch;
17+
padding: tokens.$coral-spacing-m tokens.$coral-spacing-s;
18+
width: $popover-screen-width;
19+
font-weight: 800;
20+
min-width: auto;
21+
22+
.menu-items-filter {
23+
margin: 0;
24+
}
25+
}
26+
27+
&-body {
28+
width: inherit;
29+
}
30+
31+
&-footer {
32+
display: flex;
33+
justify-content: flex-end;
34+
}
35+
36+
:global(.tc-tooltip-content) {
37+
flex-direction: column;
38+
min-height: 16rem;
39+
max-height: tokens.$coral-sizing-maximal;
40+
}
41+
42+
:global(.tc-tooltip-footer) {
43+
min-width: auto;
44+
width: $popover-screen-width;
45+
}
46+
}

0 commit comments

Comments
 (0)