Skip to content

Commit 184155f

Browse files
Merge pull request #633 from buildo/BEN-84-pagination-component
Implement Pagination component
2 parents f7dab62 + 9267730 commit 184155f

13 files changed

+302
-17
lines changed

packages/bento-design-system/src/BentoConfig.ts

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { ListConfig } from "./List/Config";
2626
import { MenuConfig } from "./Menu/Config";
2727
import { ModalConfig } from "./Modal/Config";
2828
import { NavigationConfig } from "./Navigation/Config";
29+
import { PaginationConfig } from "./Pagination/Config";
2930
import { ProgressBarConfig } from "./ProgressBar/Config";
3031
import { ReadOnlyFieldConfig } from "./ReadOnlyField/Config";
3132
import { SearchBarConfig } from "./SearchBar/Config";
@@ -67,6 +68,7 @@ export type BentoConfig = {
6768
menu: MenuConfig;
6869
modal: ModalConfig;
6970
navigation: NavigationConfig;
71+
pagination: PaginationConfig;
7072
readOnlyField: ReadOnlyFieldConfig;
7173
searchBar: SearchBarConfig;
7274
dropdown: DropdownConfig;

packages/bento-design-system/src/BentoConfigContext.tsx

+14-11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ export function useBentoConfig() {
1010
return useContext(BentoConfigContext);
1111
}
1212

13+
export const deepmerge = deepmergeCustom({
14+
mergeRecords: (value, utils) => {
15+
// NOTE(vince): in case of a JSX.Element in the config (like the Navigation's activeVisualElement),
16+
// we don't want to merge the props of the two elements, but we want to take just the second element instead.
17+
if (React.isValidElement(value[0]) || React.isValidElement(value[1])) {
18+
return value[1];
19+
}
20+
return utils.actions.defaultMerge;
21+
},
22+
// NOTE(fede): in case of an array in the config (like AreaLoader's dots),
23+
// we don't want to merge the two arrays, but we want to take just the second one instead.
24+
mergeArrays: (value) => value[1],
25+
});
26+
1327
export function BentoConfigProvider({
1428
value: config,
1529
children,
@@ -23,17 +37,6 @@ export function BentoConfigProvider({
2337
// in case this is the top level provider.
2438
const parentConfig = useBentoConfig();
2539

26-
const deepmerge = deepmergeCustom({
27-
mergeRecords: (value, utils) => {
28-
// NOTE(vince): in case of a JSX.Element in the config (like the Navigation's activeVisualElement),
29-
// we don't want to merge the props of the two elements, but we want to take just the second element instead.
30-
if (React.isValidElement(value[0]) || React.isValidElement(value[1])) {
31-
return value[1];
32-
}
33-
return utils.actions.defaultMerge;
34-
},
35-
});
36-
3740
return (
3841
<BentoConfigContext.Provider value={deepmerge(parentConfig, config) as BentoConfig}>
3942
{children}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ButtonProps, PaginationItemsPerPage } from "..";
2+
import { BentoSprinkles } from "../internal";
3+
4+
export type PaginationConfig = {
5+
paddingY: BentoSprinkles["paddingY"];
6+
itemsPerPageOptions: PaginationItemsPerPage;
7+
dropdownButtonKind: ButtonProps["kind"];
8+
navigationButtonKind: ButtonProps["kind"];
9+
navigationButtonSpacing: BentoSprinkles["gap"];
10+
showDivider: boolean;
11+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import {
2+
Body,
3+
Box,
4+
Button,
5+
ButtonProps,
6+
Column,
7+
Columns,
8+
Divider,
9+
IconButton,
10+
IconChevronDown,
11+
IconChevronLeft,
12+
IconChevronRight,
13+
Inline,
14+
Inset,
15+
LocalizedString,
16+
Menu,
17+
PaginationItemsPerPage,
18+
unsafeLocalizedString,
19+
} from "..";
20+
import { useBentoConfig } from "../BentoConfigContext";
21+
22+
export type PaginationMessages = {
23+
itemsPerPageOptionsLabel: LocalizedString;
24+
currentPageItemsLabel: (start: number, end: number, total: number) => LocalizedString;
25+
pageCountLabel: (pageCount: number) => LocalizedString;
26+
singlePageLabel: LocalizedString;
27+
previousPageButtonLabel: LocalizedString;
28+
nextPageButtonLabel: LocalizedString;
29+
};
30+
31+
export type ItemsPerPageOption = PaginationItemsPerPage[number];
32+
33+
type Props = {
34+
page: number;
35+
pageCount: number;
36+
itemsPerPage: ItemsPerPageOption;
37+
itemsPerPageOptions: Record<ItemsPerPageOption, LocalizedString>;
38+
onPageChange: (page: number) => void;
39+
onItemsPerPageChange: (itemsPerPage: ItemsPerPageOption) => void;
40+
messages: PaginationMessages;
41+
};
42+
43+
export type { Props as PaginationProps };
44+
45+
type DropdownButtonProps = Pick<ButtonProps, "label" | "onPress" | "isDisabled">;
46+
47+
function DropdownButton({ label, onPress, isDisabled }: DropdownButtonProps) {
48+
const config = useBentoConfig().pagination;
49+
return (
50+
<Button
51+
kind={config.dropdownButtonKind}
52+
hierarchy="secondary"
53+
label={label}
54+
icon={IconChevronDown}
55+
iconPosition="trailing"
56+
onPress={onPress}
57+
isDisabled={isDisabled}
58+
/>
59+
);
60+
}
61+
62+
export function Pagination(props: Props) {
63+
const {
64+
page,
65+
pageCount,
66+
itemsPerPage,
67+
itemsPerPageOptions,
68+
onPageChange,
69+
onItemsPerPageChange,
70+
messages,
71+
} = props;
72+
73+
const config = useBentoConfig().pagination;
74+
75+
const divider = config.showDivider && (
76+
<Column width="content">
77+
<Divider orientation="vertical" />
78+
</Column>
79+
);
80+
81+
return (
82+
<Inset spaceX={24}>
83+
<Columns space={24} alignY="stretch">
84+
<Column width="content">
85+
<Box height="full" display="flex" alignItems="center" paddingY={config.paddingY}>
86+
<Inline space={8} alignY="center">
87+
<Menu
88+
size="medium"
89+
trigger={(ref, triggerProps, { toggle }) => (
90+
<Box ref={ref} display="inline-block" {...triggerProps} outline="none">
91+
<DropdownButton label={unsafeLocalizedString(itemsPerPage)} onPress={toggle} />
92+
</Box>
93+
)}
94+
items={Object.entries(itemsPerPageOptions).map(([n, label]) => ({
95+
label,
96+
onPress: () => onItemsPerPageChange(parseInt(n) as ItemsPerPageOption),
97+
}))}
98+
closeOnSelect
99+
/>
100+
<Body size="medium" color="secondary">
101+
{messages.itemsPerPageOptionsLabel}
102+
</Body>
103+
</Inline>
104+
</Box>
105+
</Column>
106+
{divider}
107+
<Box height="full" display="flex" alignItems="center">
108+
<Body size="medium" color="secondary">
109+
{messages.currentPageItemsLabel(
110+
(page - 1) * itemsPerPage + 1,
111+
page * itemsPerPage,
112+
pageCount * itemsPerPage
113+
)}
114+
</Body>
115+
</Box>
116+
{divider}
117+
<Box
118+
height="full"
119+
width="full"
120+
display="flex"
121+
flexDirection="column"
122+
justifyContent="center"
123+
paddingY={config.paddingY}
124+
>
125+
<Columns space={8} alignY="center">
126+
{pageCount === 1 ? (
127+
<Body size="medium" color="secondary">
128+
{messages.singlePageLabel}
129+
</Body>
130+
) : (
131+
<Inline space={8} alignY="center">
132+
<Menu
133+
size="medium"
134+
trigger={(ref, triggerProps, { toggle }) => (
135+
<Box ref={ref} display="inline-block" {...triggerProps} outline="none">
136+
<DropdownButton
137+
label={unsafeLocalizedString(page)}
138+
onPress={toggle}
139+
isDisabled={pageCount === 1}
140+
/>
141+
</Box>
142+
)}
143+
items={Array.from(Array(pageCount).keys()).map((n) => ({
144+
label: unsafeLocalizedString(n + 1),
145+
onPress: () => onPageChange(n + 1),
146+
}))}
147+
closeOnSelect
148+
/>
149+
<Body size="medium" color="secondary">
150+
{messages.pageCountLabel(pageCount)}
151+
</Body>
152+
</Inline>
153+
)}
154+
<Column width="content">
155+
<Inline space={config.navigationButtonSpacing}>
156+
<IconButton
157+
kind={config.navigationButtonKind}
158+
hierarchy="secondary"
159+
icon={IconChevronLeft}
160+
label={messages.previousPageButtonLabel}
161+
size={24}
162+
onPress={() => onPageChange(page - 1)}
163+
isDisabled={page === 1}
164+
/>
165+
<IconButton
166+
kind={config.navigationButtonKind}
167+
hierarchy="secondary"
168+
icon={IconChevronRight}
169+
label={messages.nextPageButtonLabel}
170+
size={24}
171+
onPress={() => onPageChange(page + 1)}
172+
isDisabled={page === pageCount}
173+
/>
174+
</Inline>
175+
</Column>
176+
</Columns>
177+
</Box>
178+
</Columns>
179+
</Inset>
180+
);
181+
}

packages/bento-design-system/src/createBentoComponents.ts

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
Menu,
4747
Modal,
4848
Navigation,
49+
Pagination,
4950
ProgressBar,
5051
SearchBar,
5152
Stepper,
@@ -156,6 +157,7 @@ function internalCreateBentoComponents(
156157
Modal,
157158
Navigation,
158159
NumberField,
160+
Pagination,
159161
Placeholder,
160162
ProgressBar,
161163
Popover,

packages/bento-design-system/src/defaultTheme.css.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,9 @@ createGlobalTheme(":root", bentoVars, {
197197
primaryTransparentEnabledBackground: "transparent",
198198
primaryTransparentHoverBackground: colors.interactive10_40,
199199
primaryTransparentFocusBackground: colors.interactive10_40,
200-
secondarySolidEnabledBackground: colors.neutral90,
201-
secondarySolidHoverBackground: colors.neutral90,
202-
secondarySolidFocusBackground: colors.neutral90,
200+
secondarySolidEnabledBackground: colors.neutral05,
201+
secondarySolidHoverBackground: colors.neutral20,
202+
secondarySolidFocusBackground: colors.neutral20,
203203
secondaryTransparentEnabledBackground: "transparent",
204204
secondaryTransparentHoverBackground: colors.neutral20_40,
205205
secondaryTransparentFocusBackground: colors.neutral20_40,
@@ -219,9 +219,9 @@ createGlobalTheme(":root", bentoVars, {
219219
primaryTransparentEnabledForeground: colors.interactive40,
220220
primaryTransparentHoverForeground: colors.interactive60,
221221
primaryTransparentFocusForeground: colors.interactive60,
222-
secondarySolidEnabledForeground: colors.neutral05,
223-
secondarySolidHoverForeground: colors.neutral20,
224-
secondarySolidFocusForeground: colors.neutral20,
222+
secondarySolidEnabledForeground: colors.neutral90,
223+
secondarySolidHoverForeground: colors.neutral90,
224+
secondarySolidFocusForeground: colors.neutral90,
225225
secondaryTransparentEnabledForeground: colors.neutral80,
226226
secondaryTransparentHoverForeground: colors.black,
227227
secondaryTransparentFocusForeground: colors.black,

packages/bento-design-system/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export * from "./Modal/Modal";
4848
export * from "./Navigation/Navigation";
4949
export * from "./NumberField/NumberField";
5050
export * from "./NumberInput/NumberInput";
51+
export * from "./Pagination/Pagination";
5152
export * from "./Placeholder/Placeholder";
5253
export * from "./Popover/Popover";
5354
export * from "./ProgressBar/ProgressBar";
@@ -69,6 +70,7 @@ export type {
6970
TypeOverrides,
7071
LocalizedString,
7172
ChipCustomColors as CustomChipColors,
73+
PaginationItemsPerPage,
7274
SprinklesFn,
7375
} from "./util/ConfigurableTypes";
7476
export * from "./Toast/Toast";

packages/bento-design-system/src/util/ConfigurableTypes.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ interface ConfigurableTypes {
55
LocalizedString: string;
66
SprinklesFn: typeof bentoSprinkles;
77
ChipCustomColors: never;
8+
PaginationItemsPerPage: [10, 25, 50, 100];
89
}
910

1011
/**
@@ -26,3 +27,5 @@ export type LocalizedString = string & ConfiguredTypes["LocalizedString"];
2627
export type SprinklesFn = typeof bentoSprinkles & ConfiguredTypes["SprinklesFn"];
2728

2829
export type ChipCustomColors = string & ConfiguredTypes["ChipCustomColors"];
30+
31+
export type PaginationItemsPerPage = Array<number> & ConfiguredTypes["PaginationItemsPerPage"];

packages/bento-design-system/src/util/defaultConfigs.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import type { ReadOnlyFieldConfig } from "../ReadOnlyField/Config";
5858
import type { FileUploaderFieldConfig } from "../FileUploaderField/Config";
5959
import type { ChartConfig } from "../Charts/Config";
6060
import { IconCopy } from "../Icons/IconCopy";
61+
import { PaginationConfig } from "../Pagination/Config";
6162

6263
export const actions: ActionsConfig = {
6364
primaryActionButtonKind: "solid",
@@ -428,6 +429,15 @@ export const navigation: NavigationConfig = {
428429
},
429430
};
430431

432+
export const pagination: PaginationConfig = {
433+
paddingY: 12,
434+
itemsPerPageOptions: [10, 25, 50, 100],
435+
dropdownButtonKind: "transparent",
436+
navigationButtonKind: "transparent",
437+
navigationButtonSpacing: 24,
438+
showDivider: true,
439+
};
440+
431441
export const readOnlyField: ReadOnlyFieldConfig = {
432442
copyIcon: IconCopy,
433443
copyIconSize: 24,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { deepmerge } from "../src/BentoConfigContext";
2+
3+
describe("deepmerge", () => {
4+
it("merges two objects", () => {
5+
const input1 = { a: 1, b: 2 };
6+
const input2 = { c: 3, d: 4 };
7+
const output = { a: 1, b: 2, c: 3, d: 4 };
8+
expect(deepmerge(input1, input2)).toEqual(output);
9+
});
10+
11+
it("does not merge two arrays", () => {
12+
const input1 = [1, 2, 3];
13+
const input2 = [4, 5, 6];
14+
expect(deepmerge(input1, input2)).toEqual(input2);
15+
});
16+
17+
it("does not merge object fields that are valid React elements", () => {
18+
const input1 = { a: 1, b: 2 };
19+
const input2 = <div>hello</div>;
20+
expect(deepmerge(input1, input2)).toEqual(input2);
21+
});
22+
});

0 commit comments

Comments
 (0)