Skip to content

Commit

Permalink
Merge branch 'master' into card-variable-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
JoelArmendariz authored Aug 21, 2020
2 parents f2655ef + 2cc7a63 commit 4370579
Show file tree
Hide file tree
Showing 14 changed files with 5,018 additions and 353 deletions.
166 changes: 166 additions & 0 deletions src/components/Table/StatefulTable.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,170 @@ describe('stateful table with real reducer', () => {

expect(mockActions.table.onApplyRowAction).toHaveBeenCalled();
});
it('multiselect should filter properly with pre-selected filter', async () => {
render(
<StatefulTable
{...initialState}
columns={initialState.columns.map(column => {
if (column.filter) {
return {
...column,
filter: { ...column.filter, isMultiselect: !!column.filter?.options },
};
}
return column;
})}
view={{
...initialState.view,
filters: [{ columnId: 'select', value: 'option-B' }], // start with filtering by option-B
pagination: {
...initialState.view.pagination,
maxPages: 10,
},
toolbar: {
activeBar: 'filter',
},
}}
secondaryTitle={`Row count: ${initialState.data.length}`}
actions={mockActions}
isSortable
options={{
...initialState.options,
hasFilter: 'onKeyPress',
wrapCellText: 'always',
hasSingleRowEdit: true,
}}
/>
);

// start off with a filter of option-B.
// Note: each options length count has an extra item due to the multiselect having the same title attribute as the row cell
const initialFilteredRowsOptionA = screen.getAllByTitle('option-A');
const initialFilteredRowsOptionB = screen.getAllByTitle('option-B');
const initialFilteredRowsOptionC = screen.getAllByTitle('option-C');
const initialItemCount = screen.getByText('1–10 of 33 items'); // confirm row count in the pagination
expect(initialFilteredRowsOptionA).toHaveLength(1); // 1 instead of 0 because it sees the multiselect option
expect(initialFilteredRowsOptionB).toHaveLength(11); // 11 instead of 10 because it sees the multiselect option
expect(initialFilteredRowsOptionC).toHaveLength(1); // 1 instead of 0 because it sees the multiselect option
expect(initialItemCount).toBeInTheDocument();

// next add an additional filter with option-A
// Note: each options length count has an extra item due to the multiselect having the same title attribute as the row cell
fireEvent.click(screen.getAllByRole('option')[0]); // fire click on option-A in our multiselect

const secondFilteredRowsOptionA = await screen.findAllByTitle('option-A');
const secondFilteredRowsOptionB = await screen.findAllByTitle('option-B');
const secondFilteredRowsOptionC = await screen.findAllByTitle('option-C');
const secondItemCount = screen.getByText('1–10 of 67 items'); // confirm row count in the pagination
expect(secondFilteredRowsOptionA).toHaveLength(6);
expect(secondFilteredRowsOptionB).toHaveLength(6);
expect(secondFilteredRowsOptionC).toHaveLength(1);
expect(secondItemCount).toBeInTheDocument();

// next remove filter for option-B
// Note: each options length count has an extra item due to the multiselect having the same title attribute as the row cell
fireEvent.click(screen.getAllByRole('option')[1]); // fire click on option-B in our multiselect

const thirdFilteredRowsOptionA = await screen.findAllByTitle('option-A');
const thirdFilteredRowsOptionB = await screen.findAllByTitle('option-B');
const thirdFilteredRowsOptionC = await screen.findAllByTitle('option-C');
const thirdItemCount = screen.getByText('1–10 of 34 items'); // confirm row count in the pagination
expect(thirdFilteredRowsOptionA).toHaveLength(11);
expect(thirdFilteredRowsOptionB).toHaveLength(1);
expect(thirdFilteredRowsOptionC).toHaveLength(1);
expect(thirdItemCount).toBeInTheDocument();

// next clear all filters from the multiselect
// Note: each options length count has an extra item due to the multiselect having the same title attribute as the row cell
const clearSelectBox = screen.getByLabelText('Clear Selection');
expect(clearSelectBox).toBeInTheDocument();

fireEvent.click(clearSelectBox);

const fourthFilteredRowsOptionA = await screen.findAllByTitle('option-A');
const fourthFilteredRowsOptionB = await screen.findAllByTitle('option-B');
const fourthFilteredRowsOptionC = await screen.findAllByTitle('option-C');
const fourthItemCount = screen.getByText('1–10 of 100 items'); // confirm row count in the pagination
expect(fourthFilteredRowsOptionA).toHaveLength(5);
expect(fourthFilteredRowsOptionB).toHaveLength(4);
expect(fourthFilteredRowsOptionC).toHaveLength(4);
expect(fourthItemCount).toBeInTheDocument();
});

it('multiselect should filter properly with no pre-selected filters', async () => {
render(
<StatefulTable
{...initialState}
columns={initialState.columns.map(column => {
if (column.filter) {
return {
...column,
filter: { ...column.filter, isMultiselect: !!column.filter?.options },
};
}
return column;
})}
view={{
...initialState.view,
filters: [], // start with no filters
pagination: {
...initialState.view.pagination,
maxPages: 10,
},
toolbar: {
activeBar: 'filter',
},
}}
secondaryTitle={`Row count: ${initialState.data.length}`}
actions={mockActions}
isSortable
options={{
...initialState.options,
hasFilter: 'onKeyPress',
wrapCellText: 'always',
hasSingleRowEdit: true,
}}
/>
);

// start off with no filters.
// Note: each options length count has an extra item due to the multiselect having the same title attribute as the row cell
const initialFilteredRowsOptionA = screen.getAllByTitle('option-A');
const initialFilteredRowsOptionB = screen.getAllByTitle('option-B');
const initialFilteredRowsOptionC = screen.getAllByTitle('option-C');
const initialItemCount = screen.getByText('1–10 of 100 items'); // confirm row count in the pagination
expect(initialFilteredRowsOptionA).toHaveLength(5);
expect(initialFilteredRowsOptionB).toHaveLength(4);
expect(initialFilteredRowsOptionC).toHaveLength(4);
expect(initialItemCount).toBeInTheDocument();

// next add an a filter with option-A
// Note: each options length count has an extra item due to the multiselect having the same title attribute as the row cell
fireEvent.click(screen.getAllByRole('option')[0]); // fire click on option-A in our multiselect

const secondFilteredRowsOptionA = await screen.findAllByTitle('option-A');
const secondFilteredRowsOptionB = await screen.findAllByTitle('option-B');
const secondFilteredRowsOptionC = await screen.findAllByTitle('option-C');
const secondItemCount = screen.getByText('1–10 of 34 items'); // confirm row count in the pagination
expect(secondFilteredRowsOptionA).toHaveLength(11);
expect(secondFilteredRowsOptionB).toHaveLength(1);
expect(secondFilteredRowsOptionC).toHaveLength(1);
expect(secondItemCount).toBeInTheDocument();

// next clear all filters from the multiselect
// Note: each options length count has an extra item due to the multiselect having the same title attribute as the row cell
const clearSelectBox = screen.getByLabelText('Clear Selection');
expect(clearSelectBox).toBeInTheDocument();

fireEvent.click(clearSelectBox);

const fourthFilteredRowsOptionA = await screen.findAllByTitle('option-A');
const fourthFilteredRowsOptionB = await screen.findAllByTitle('option-B');
const fourthFilteredRowsOptionC = await screen.findAllByTitle('option-C');
const fourthItemCount = screen.getByText('1–10 of 100 items'); // confirm row count in the pagination
expect(fourthFilteredRowsOptionA).toHaveLength(5);
expect(fourthFilteredRowsOptionB).toHaveLength(4);
expect(fourthFilteredRowsOptionC).toHaveLength(4);
expect(fourthItemCount).toBeInTheDocument();
});
});
7 changes: 6 additions & 1 deletion src/components/Table/Table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ const propTypes = {
filters: PropTypes.arrayOf(
PropTypes.shape({
columnId: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]).isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
PropTypes.arrayOf(PropTypes.string),
]).isRequired,
})
),
toolbar: PropTypes.shape({
Expand Down
46 changes: 46 additions & 0 deletions src/components/Table/Table.story.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,52 @@ storiesOf('Watson IoT/Table', module)
},
}
)
.add(
'Stateful Example with multiselect filtering',
() => (
<FullWidthWrapper>
<StatefulTable
{...initialState}
columns={initialState.columns.map(column => {
if (column.filter) {
return {
...column,
filter: { ...column.filter, isMultiselect: !!column.filter?.options },
};
}
return column;
})}
view={{
...initialState.view,
pagination: {
...initialState.view.pagination,
maxPages: 5,
},
toolbar: {
activeBar: 'filter',
},
}}
secondaryTitle={text('Secondary Title', `Row count: ${initialState.data.length}`)}
actions={actions}
isSortable
lightweight={boolean('lightweight', false)}
options={{
...initialState.options,
hasFilter: select('hasFilter', ['onKeyPress', 'onEnterAndBlur'], 'onKeyPress'),
wrapCellText: select('wrapCellText', selectTextWrapping, 'always'),
hasSingleRowEdit: true,
}}
/>
</FullWidthWrapper>
),
{
info: {
text: `This table has a multiselect filter. To support multiselect filtering, make sure to pass isMultiselect: true to the filter prop on the table.`,
propTables: [Table],
propTablesExclude: [StatefulTable],
},
}
)
.add(
'Stateful Example with row nesting and fixed columns',
() => <StatefulTableWithNestedRowItems />,
Expand Down
104 changes: 73 additions & 31 deletions src/components/Table/TableHead/FilterHeaderRow/FilterHeaderRow.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ComboBox, DataTable, FormItem, TextInput } from 'carbon-components-react';
import { ComboBox, DataTable, FormItem, TextInput, MultiSelect } from 'carbon-components-react';
import { Close16 } from '@carbon/icons-react';
import memoize from 'lodash/memoize';
import classnames from 'classnames';
Expand Down Expand Up @@ -30,6 +30,8 @@ class FilterHeaderRow extends Component {
text: PropTypes.string.isRequired,
})
),
/** if isMultiselect and isFilterable are true, the table is filtered based on a multiselect */
isMultiselect: PropTypes.bool,
})
).isRequired,
/** internationalized string */
Expand All @@ -48,7 +50,12 @@ class FilterHeaderRow extends Component {
filters: PropTypes.arrayOf(
PropTypes.shape({
columnId: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]).isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.bool,
PropTypes.arrayOf(PropTypes.string),
]).isRequired,
})
),
/** Callback when filter is applied sends object of keys and values with the filter values */
Expand Down Expand Up @@ -192,35 +199,70 @@ class FilterHeaderRow extends Component {
column.isFilterable !== undefined && !column.isFilterable ? (
<div />
) : column.options ? (
<ComboBox
key={columnStateValue}
className={`${iotPrefix}--filterheader-combo`}
id={`column-${i}`}
aria-label={filterText}
translateWithId={this.handleTranslation}
items={memoizeColumnOptions(column.options)}
itemToString={item => (item ? item.text : '')}
initialSelectedItem={{
id: columnStateValue,
text: (
column.options.find(option => option.id === columnStateValue) || { text: '' }
).text, // eslint-disable-line react/destructuring-assignment
}}
placeholder={column.placeholderText || 'Choose an option'}
onChange={evt => {
this.setState(
state => ({
filterValues: {
...state.filterValues,
[column.id]: evt.selectedItem === null ? '' : evt.selectedItem.id,
},
}),
this.handleApplyFilter
);
}}
light={lightweight}
disabled={isDisabled}
/>
column.isMultiselect ? (
<MultiSelect
key={columnStateValue}
className={`${iotPrefix}--filterheader-multiselect`}
id={`column-${i}`}
aria-label={filterText}
translateWithId={this.handleTranslation}
items={memoizeColumnOptions(column.options)}
label={column.placeholderText || 'Choose an option'}
itemToString={item => (item ? item.text : '')}
initialSelectedItems={
Array.isArray(columnStateValue)
? columnStateValue.map(value =>
typeof value !== 'object' ? { id: value, text: value } : value
)
: [{ id: columnStateValue, text: columnStateValue }]
}
onChange={evt => {
this.setState(
state => ({
filterValues: {
...state.filterValues,
[column.id]: evt.selectedItems.map(item => item.text),
},
}),
this.handleApplyFilter
);
}}
light
disabled={isDisabled}
/>
) : (
<ComboBox
key={columnStateValue}
className={`${iotPrefix}--filterheader-combo`}
id={`column-${i}`}
aria-label={filterText}
translateWithId={this.handleTranslation}
items={memoizeColumnOptions(column.options)}
itemToString={item => (item ? item.text : '')}
initialSelectedItem={{
id: columnStateValue,
text: (
column.options.find(option => option.id === columnStateValue) || {
text: '',
}
).text, // eslint-disable-line react/destructuring-assignment
}}
placeholder={column.placeholderText || 'Choose an option'}
onChange={evt => {
this.setState(
state => ({
filterValues: {
...state.filterValues,
[column.id]: evt.selectedItem === null ? '' : evt.selectedItem.id,
},
}),
this.handleApplyFilter
);
}}
light={lightweight}
disabled={isDisabled}
/>
)
) : (
<FormItem className={`${iotPrefix}--filter-header-row--form-item`}>
<TextInput
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@
padding-right: $spacing-03;
}

/* Need to force the colors of the selected-items counter in
the multiselect box back to their original colors as they
were being overridden by other styles */
.#{$iot-prefix}--filterheader-multiselect {
.bx--list-box__selection--multi {
background-color: $interactive-02;
}
.bx--list-box__selection--multi > svg {
fill: $ui-background;
}
}

.#{$prefix}--tag--filter {
background-color: transparent;

Expand Down
Loading

0 comments on commit 4370579

Please sign in to comment.