Skip to content
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

[OSCI][FIX] responsive basic table components #1119

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- Add exit code to compile-scss script on failure ([#1024](https://github.com/opensearch-project/oui/pull/1024))
- Extract build archive into a folder for OSD integration test CI ([#1075](https://github.com/opensearch-project/oui/pull/1075))
- Correct file path for import of Query component ([#1069](https://github.com/opensearch-project/oui/pull/1069))
- Fix responsive basic table components for multiple actions ([#1119](https://github.com/opensearch-project/oui/pull/1119))
- Fix "Guidelines" documentation links rendering blank pages ([#1111](https://github.com/opensearch-project/oui/pull/1111))

### 🚞 Infrastructure
Expand Down
31 changes: 25 additions & 6 deletions src/components/basic_table/basic_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ import {
formatDate,
formatNumber,
formatText,
getBreakpoint,
LEFT_ALIGNMENT,
OuiBreakpointSize,
RIGHT_ALIGNMENT,
SortDirection,
throttle,
} from '../../services';
import { CommonProps } from '../common';
import { isFunction } from '../../services/predicate';
Expand Down Expand Up @@ -310,6 +313,7 @@ export type OuiBasicTableProps<T> = CommonProps &
interface State<T> {
initialSelectionRendered: boolean;
selection: T[];
currentBreakpoint: OuiBreakpointSize | undefined;
}

interface SortOptions {
Expand All @@ -326,7 +330,7 @@ function hasPagination<T>(
return x.hasOwnProperty('pagination') && !!x.pagination;
}

export class OuiBasicTable<T = any> extends Component<
export class OuiBasicTable<T extends {} = any> extends Component<
OuiBasicTableProps<T>,
State<T>
> {
Expand Down Expand Up @@ -375,12 +379,16 @@ export class OuiBasicTable<T = any> extends Component<
// used for checking if initial selection is rendered
initialSelectionRendered: false,
selection: [],
currentBreakpoint: getBreakpoint(
typeof window === 'undefined' ? -Infinity : window.innerWidth
),
Comment on lines +382 to +384
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thanhinhchtom I like the idea here, but I don't actually think this behavior should be dependent on the breakpoint, which is defined by the width of the browser window. Instead, what really matters is the width of the action column - we should only render the action icons on hover if there's room for them to render next to the three-dot-menu icon.

There is one complication, though, which is the fact that the table becomes responsive at small breakpoints, and the icons no longer appear on hover.

So to summarize, rather than set up a listener on the window size, you should just check the width of the action column (assuming breakpoint > s). And render however many icons (0, 1, or 2) will fit within that column width. I think you still may need a listener for cases where the overall table width changes, because column size can be set as a percentage of the table.

Copy link
Author

@thanhinhchtom thanhinhchtom Nov 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshuarrrr So I should not use breakpoint here. Instead I should use size of the column and just fit more button if it is still enough space, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's correct. Although you may also still want to have your logic skip small breakpoints, because the different layout there already handles this problem.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I will try that and ask you later if needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshuarrrr One more quick question. I want to clarify the requirement. So I can still use the breakpoint there if needed, but I just cannot hardcode for the different situation as what I did and I have to find the way to use the current size of the component to decide how many buttons will show up, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The breakpoint usage should just be to determine whether column widths matter at all or not. But yeah the conditional logic for what and how to render should depend on the action column width. And the action column width may be itself dependent on the table/component width.

};
}

componentDidMount() {
if (this.props.loading && this.tbody) this.addLoadingListeners(this.tbody);
this.getInitialSelection();
window.addEventListener('resize', this.functionToCallOnWindowResize);
}

componentDidUpdate(prevProps: OuiBasicTableProps<T>) {
Expand All @@ -396,6 +404,7 @@ export class OuiBasicTable<T = any> extends Component<

componentWillUnmount() {
this.removeLoadingListeners();
window.removeEventListener('resize', this.functionToCallOnWindowResize);
}

getInitialSelection() {
Expand Down Expand Up @@ -457,6 +466,14 @@ export class OuiBasicTable<T = any> extends Component<
this.cleanups.length = 0;
};

private functionToCallOnWindowResize = throttle(() => {
const newBreakpoint = getBreakpoint(window.innerWidth);
if (newBreakpoint !== this.state.currentBreakpoint) {
this.setState({ currentBreakpoint: newBreakpoint });
}
// reacts every 50ms to resize changes and always gets the final update
}, 50);

buildCriteria(props: OuiBasicTableProps<T>): Criteria<T> {
const criteria: Criteria<T> = {};
if (hasPagination(props)) {
Expand Down Expand Up @@ -898,13 +915,13 @@ export class OuiBasicTable<T = any> extends Component<
}
headers.push(
<OuiTableHeaderCell
key={`_data_h_${field}_${index}`}
key={`_data_h_${String(field)}_${index}`}
align={columnAlign}
width={width}
isMobileHeader={isMobileHeader}
hideForMobile={hideForMobile}
mobileOptions={mobileOptions}
data-test-subj={`tableHeaderCell_${field}_${index}`}
data-test-subj={`tableHeaderCell_${String(field)}_${index}`}
description={description}
{...sorting}>
{name}
Expand Down Expand Up @@ -946,7 +963,7 @@ export class OuiBasicTable<T = any> extends Component<
if (footer) {
footers.push(
<OuiTableFooterCell
key={`footer_${field}_${footers.length - 1}`}
key={`footer_${String(field)}_${footers.length - 1}`}
align={align}>
{footer}
</OuiTableFooterCell>
Expand Down Expand Up @@ -1207,10 +1224,12 @@ export class OuiBasicTable<T = any> extends Component<
let actualActions = column.actions.filter(
(action: Action<T>) => !action.available || action.available(item)
);

if (actualActions.length > 2) {
// if any of the actions `isPrimary`, add them inline as well, but only the first 2
const primaryActions = actualActions.filter((o) => o.isPrimary);
actualActions = primaryActions.slice(0, 2);
const numItemMax = this.state.currentBreakpoint === 'm' ? 0 : 2;
actualActions = primaryActions.slice(0, numItemMax);

// if we have more than 1 action, we don't show them all in the cell, instead we
// put them all in a popover tool. This effectively means we can only have a maximum
Expand Down Expand Up @@ -1264,7 +1283,7 @@ export class OuiBasicTable<T = any> extends Component<
) {
const { field, render, dataType } = column;

const key = `_data_column_${field}_${itemId}_${columnIndex}`;
const key = `_data_column_${String(field)}_${itemId}_${columnIndex}`;
const contentRenderer = render || this.getRendererForDataType(dataType);
const value = get(item, field as string);
const content = contentRenderer(value, item);
Expand Down