Skip to content

Commit 7eb9dfa

Browse files
authored
feat(Table): favoritable sortable header cell and example (#10823)
* feat(Table): favoritable sortable header cell and example * fix(TableFavoritable): better favorite / unfavorite state of the header favorite button * refactor(TableFavoritable): custom aria-labels, refactoring
1 parent 9a679a4 commit 7eb9dfa

6 files changed

Lines changed: 82 additions & 14 deletions

File tree

packages/react-table/src/components/Table/SortColumn.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import * as React from 'react';
22
import LongArrowAltUpIcon from '@patternfly/react-icons/dist/esm/icons/long-arrow-alt-up-icon';
33
import LongArrowAltDownIcon from '@patternfly/react-icons/dist/esm/icons/long-arrow-alt-down-icon';
44
import ArrowsAltVIcon from '@patternfly/react-icons/dist/esm/icons/arrows-alt-v-icon';
5+
import StarIcon from '@patternfly/react-icons/dist/esm/icons/star-icon';
56
import { css } from '@patternfly/react-styles';
67
import styles from '@patternfly/react-styles/css/components/Table/table';
78
import { TableText } from './TableText';
89
import { TooltipProps } from '@patternfly/react-core/dist/esm/components/Tooltip';
10+
import { ActionList, ActionListItem, Button } from '@patternfly/react-core';
11+
import { FavoriteButtonProps } from './base/types';
912

1013
export enum SortByDirection {
1114
asc = 'asc',
@@ -21,6 +24,7 @@ export interface SortColumnProps extends React.ButtonHTMLAttributes<HTMLButtonEl
2124
tooltip?: React.ReactNode;
2225
tooltipProps?: Omit<TooltipProps, 'content'>;
2326
tooltipHasDefaultBehavior?: boolean;
27+
favoriteButtonProps?: FavoriteButtonProps;
2428
}
2529

2630
export const SortColumn: React.FunctionComponent<SortColumnProps> = ({
@@ -33,6 +37,7 @@ export const SortColumn: React.FunctionComponent<SortColumnProps> = ({
3337
tooltip,
3438
tooltipProps,
3539
tooltipHasDefaultBehavior,
40+
favoriteButtonProps,
3641
...props
3742
}: SortColumnProps) => {
3843
let SortedByIcon;
@@ -42,6 +47,29 @@ export const SortColumn: React.FunctionComponent<SortColumnProps> = ({
4247
} else {
4348
SortedByIcon = ArrowsAltVIcon;
4449
}
50+
51+
if (favoriteButtonProps) {
52+
return (
53+
<ActionList isIconList>
54+
<ActionListItem>
55+
<Button variant="plain" icon={<StarIcon />} {...favoriteButtonProps} />
56+
</ActionListItem>
57+
<ActionListItem>
58+
<Button
59+
variant="plain"
60+
icon={
61+
<span className={css(styles.tableSortIndicator)}>
62+
<SortedByIcon />
63+
</span>
64+
}
65+
onClick={(event) => onSort && onSort(event)}
66+
{...props}
67+
/>
68+
</ActionListItem>
69+
</ActionList>
70+
);
71+
}
72+
4573
return (
4674
<button
4775
{...props}

packages/react-table/src/components/Table/TableTypes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export interface IColumn {
109109
allRowsExpanded?: boolean;
110110
isHeaderSelectDisabled?: boolean;
111111
onFavorite?: OnFavorite;
112+
favoriteButtonProps?: ButtonProps;
112113
};
113114
}
114115

packages/react-table/src/components/Table/Th.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,18 @@ const ThBase: React.FunctionComponent<ThProps> = ({
113113
}
114114
onMouseEnterProp(event);
115115
};
116+
116117
let sortParams = null;
117118
if (sort) {
118119
if (sort.isFavorites) {
119120
sortParams = sortableFavorites({
120-
onSort: sort?.onSort,
121+
onSort: sort.onSort,
121122
columnIndex: sort.columnIndex,
122123
sortBy: sort.sortBy,
123124
tooltip: tooltip as string,
124-
tooltipProps
125+
tooltipProps,
126+
ariaLabel: sort['aria-label'],
127+
favoriteButtonProps: sort.favoriteButtonProps
125128
})();
126129
} else {
127130
sortParams = sortable(children as IFormatterValueType, {
@@ -137,6 +140,7 @@ const ThBase: React.FunctionComponent<ThProps> = ({
137140
});
138141
}
139142
}
143+
140144
const selectParams = select
141145
? selectable(children as IFormatterValueType, {
142146
rowData: {
@@ -217,6 +221,7 @@ const ThBase: React.FunctionComponent<ThProps> = ({
217221
hasRightBorder && scrollStyles.modifiers.borderRight,
218222
hasLeftBorder && scrollStyles.modifiers.borderLeft,
219223
modifier && styles.modifiers[modifier as 'breakWord' | 'fitContent' | 'nowrap' | 'truncate' | 'wrap'],
224+
sort?.favoriteButtonProps?.favorited && styles.modifiers.favorited,
220225
mergedClassName
221226
)}
222227
{...mergedProps}

packages/react-table/src/components/Table/base/types.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
*/
77

88
import * as React from 'react';
9-
import { TooltipProps } from '@patternfly/react-core/dist/esm/components/Tooltip';
10-
import { PopoverProps } from '@patternfly/react-core/dist/esm/components/Popover';
11-
import { SelectProps } from '@patternfly/react-core/dist/esm/components/Select';
9+
import { ButtonProps, PopoverProps, SelectProps, TooltipProps } from '@patternfly/react-core';
1210
import { Table } from '../Table';
1311
import { Thead } from '../Thead';
1412
import { Tbody } from '../Tbody';
@@ -156,15 +154,24 @@ export interface ThInfoType {
156154
className?: string;
157155
}
158156

157+
export interface FavoriteButtonProps extends ButtonProps {
158+
/** Flag if the button is favorited. */
159+
favorited?: boolean;
160+
}
161+
159162
export interface ThSortType {
160163
/** Wraps the content in a button and adds a sort icon - Click callback on the sortable cell */
161164
onSort?: OnSort;
162165
/** Provide the currently active column's index and direction */
163166
sortBy: ISortBy;
164167
/** The column index */
165168
columnIndex: number;
169+
/** Adds accessible text to the sort button. */
170+
'aria-label'?: string;
166171
/** True to make this a favoritable sorting cell */
167172
isFavorites?: boolean;
173+
/** Props for the favorite button (only for favoritable cell). */
174+
favoriteButtonProps?: FavoriteButtonProps;
168175
}
169176

170177
export interface ThSelectType {

packages/react-table/src/components/Table/examples/TableFavoritable.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ export const TableFavoritable: React.FunctionComponent = () => {
2828
// Index of the currently sorted column
2929
// Note: if you intend to make columns reorderable, you may instead want to use a non-numeric key
3030
// as the identifier of the sorted column. See the "Compound expandable" example.
31-
const [activeSortIndex, setActiveSortIndex] = React.useState<number | null>(null);
31+
const [activeSortIndex, setActiveSortIndex] = React.useState<number>();
3232

3333
// Sort direction of the currently sorted column
34-
const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc' | null>(null);
34+
const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc'>();
3535

3636
// Favorite state is similar to selection state, see Selectable with checkbox.
3737
const [favoriteRepoNames, setFavoriteRepoNames] = React.useState<string[]>([]);
@@ -42,6 +42,9 @@ export const TableFavoritable: React.FunctionComponent = () => {
4242
});
4343
const isRepoFavorited = (repo: Repository) => favoriteRepoNames.includes(repo.name);
4444

45+
// State of the header cell to favorite / unfavorite all rows
46+
const [headerFavorited, setHeaderFavorited] = React.useState(false);
47+
4548
// Since OnSort specifies sorted columns by index, we need sortable values for our object by column index.
4649
// In this example we only deal with booleans here because we only sort on the favorites column.
4750
// For more complex sorting, see Sortable.
@@ -55,7 +58,7 @@ export const TableFavoritable: React.FunctionComponent = () => {
5558
// Note that we perform the sort as part of the component's render logic and not in onSort.
5659
// We shouldn't store the list of data in state because we don't want to have to sync that with props.
5760
let sortedRepositories = repositories;
58-
if (activeSortIndex !== null) {
61+
if (activeSortIndex !== undefined) {
5962
sortedRepositories = repositories.sort((a, b) => {
6063
const aValue = getSortableRowValues(a)[activeSortIndex];
6164
const bValue = getSortableRowValues(b)[activeSortIndex];
@@ -80,7 +83,16 @@ export const TableFavoritable: React.FunctionComponent = () => {
8083
setActiveSortIndex(index);
8184
setActiveSortDirection(direction);
8285
},
83-
columnIndex
86+
'aria-label': 'Sort favorites',
87+
columnIndex,
88+
favoriteButtonProps: {
89+
favorited: headerFavorited,
90+
onClick: (_event) => {
91+
repositories.forEach((repo) => setRepoFavorited(repo, !headerFavorited));
92+
setHeaderFavorited(!headerFavorited);
93+
},
94+
'aria-label': headerFavorited ? 'Unfavorite all' : 'Favorite all'
95+
}
8496
});
8597

8698
return (
@@ -101,7 +113,20 @@ export const TableFavoritable: React.FunctionComponent = () => {
101113
<Td
102114
favorites={{
103115
isFavorited: isRepoFavorited(repo),
104-
onFavorite: (_event, isFavoriting) => setRepoFavorited(repo, isFavoriting),
116+
onFavorite: (_event, isFavoriting) => {
117+
setRepoFavorited(repo, isFavoriting);
118+
119+
if (
120+
isFavoriting &&
121+
repositories.filter((r) => r !== repo).every((r) => favoriteRepoNames.includes(r.name))
122+
) {
123+
setHeaderFavorited(true);
124+
}
125+
126+
if (!isFavoriting && favoriteRepoNames.length === 1 && favoriteRepoNames.includes(repo.name)) {
127+
setHeaderFavorited(false);
128+
}
129+
},
105130
rowIndex
106131
}}
107132
/>

packages/react-table/src/components/Table/utils/decorators/sortable.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import StarIcon from '@patternfly/react-icons/dist/esm/icons/star-icon';
88
export const sortableFavorites = (sort: any) => () =>
99
sortable(<StarIcon aria-hidden />, {
1010
columnIndex: sort.columnIndex,
11-
className: styles.modifiers.favorite,
12-
ariaLabel: 'Sort favorites',
11+
className: styles.tableFavorite,
12+
ariaLabel: sort.ariaLabel ?? 'Sort favorites',
1313
column: {
1414
extraParams: {
1515
sortBy: sort.sortBy,
16-
onSort: sort?.onSort
16+
onSort: sort.onSort,
17+
favoriteButtonProps: sort.favoriteButtonProps
1718
}
1819
},
1920
tooltip: sort.tooltip,
@@ -26,7 +27,7 @@ export const sortable: ITransform = (
2627
{ columnIndex, column, property, className, ariaLabel, tooltip, tooltipProps, tooltipHasDefaultBehavior }: IExtra
2728
) => {
2829
const {
29-
extraParams: { sortBy, onSort }
30+
extraParams: { sortBy, onSort, favoriteButtonProps }
3031
} = column;
3132

3233
const extraData = {
@@ -62,6 +63,7 @@ export const sortable: ITransform = (
6263
tooltip={tooltip}
6364
tooltipProps={tooltipProps}
6465
tooltipHasDefaultBehavior={tooltipHasDefaultBehavior}
66+
favoriteButtonProps={favoriteButtonProps}
6567
>
6668
{label as React.ReactNode}
6769
</SortColumn>

0 commit comments

Comments
 (0)