Skip to content
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 pages/pagination/permutations.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const permutations = createPermutations<PaginationProps>([
pagesCount: [15],
openEnd: [true, false],
ariaLabels: [paginationLabels],
jumpToPage: [undefined, { loading: false }, { loading: true }],
},
]);

Expand Down
99 changes: 99 additions & 0 deletions pages/table/jump-to-page-closed.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';

import { CollectionPreferences } from '~components';
import Pagination from '~components/pagination';
import Table from '~components/table';

import { generateItems, Instance } from './generate-data';

const allItems = generateItems(100);
const PAGE_SIZE = 10;

export default function JumpToPageClosedExample() {
const [currentPageIndex, setCurrentPageIndex] = useState(1);

const totalPages = Math.ceil(allItems.length / PAGE_SIZE);
const startIndex = (currentPageIndex - 1) * PAGE_SIZE;
const endIndex = startIndex + PAGE_SIZE;
const currentItems = allItems.slice(startIndex, endIndex);

return (
<Table
header={<h1>Jump to Page - Closed Pagination (100 items, 10 pages)</h1>}
columnDefinitions={[
{ header: 'ID', cell: (item: Instance) => item.id },
{ header: 'State', cell: (item: Instance) => item.state },
{ header: 'Type', cell: (item: Instance) => item.type },
{ header: 'DNS Name', cell: (item: Instance) => item.dnsName || '-' },
]}
preferences={
<CollectionPreferences
title="Preferences"
confirmLabel="Confirm"
cancelLabel="Cancel"
preferences={{
pageSize: 10,
contentDisplay: [
{ id: 'variable', visible: true },
{ id: 'value', visible: true },
{ id: 'type', visible: true },
{ id: 'description', visible: true },
],
}}
pageSizePreference={{
title: 'Page size',
options: [
{ value: 10, label: '10 resources' },
{ value: 20, label: '20 resources' },
],
}}
wrapLinesPreference={{}}
stripedRowsPreference={{}}
contentDensityPreference={{}}
contentDisplayPreference={{
options: [
{
id: 'variable',
label: 'Variable name',
alwaysVisible: true,
},
{ id: 'value', label: 'Text value' },
{ id: 'type', label: 'Type' },
{ id: 'description', label: 'Description' },
],
}}
stickyColumnsPreference={{
firstColumns: {
title: 'Stick first column(s)',
description: 'Keep the first column(s) visible while horizontally scrolling the table content.',
options: [
{ label: 'None', value: 0 },
{ label: 'First column', value: 1 },
{ label: 'First two columns', value: 2 },
],
},
lastColumns: {
title: 'Stick last column',
description: 'Keep the last column visible while horizontally scrolling the table content.',
options: [
{ label: 'None', value: 0 },
{ label: 'Last column', value: 1 },
],
},
}}
/>
}
items={currentItems}
pagination={
<Pagination
currentPageIndex={currentPageIndex}
pagesCount={totalPages}
onChange={({ detail }) => setCurrentPageIndex(detail.currentPageIndex)}
jumpToPage={{}}
/>
}
/>
);
}
139 changes: 139 additions & 0 deletions pages/table/jump-to-page-open-end.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useRef, useState } from 'react';

import Pagination, { PaginationProps } from '~components/pagination';
import Table from '~components/table';

import { generateItems, Instance } from './generate-data';

const PAGE_SIZE = 10;
const TOTAL_ITEMS = 100; // Simulated server-side total

export default function JumpToPageOpenEndExample() {
const [currentPageIndex, setCurrentPageIndex] = useState(1);
const [loadedPages, setLoadedPages] = useState<Record<number, Instance[]>>({ 1: generateItems(10) });
const [jumpToPageIsLoading, setJumpToPageIsLoading] = useState(false);
const [maxKnownPage, setMaxKnownPage] = useState(1);
const [openEnd, setOpenEnd] = useState(true);
const jumpToPageRef = useRef<PaginationProps.JumpToPageRef>(null);

const currentItems = loadedPages[currentPageIndex] || [];

const loadPage = (pageIndex: number) => {
return new Promise<Instance[]>((resolve, reject) => {
setTimeout(() => {
const totalPages = Math.ceil(TOTAL_ITEMS / PAGE_SIZE);
if (pageIndex > totalPages) {
reject({
message: `Page ${pageIndex} does not exist. Maximum page is ${totalPages}.`,
maxPage: totalPages,
});
} else {
const startIndex = (pageIndex - 1) * PAGE_SIZE;
resolve(generateItems(10).map((item, i) => ({ ...item, id: `${startIndex + i + 1}` })));
}
}, 500);
});
};

return (
<Table
header={
<div>
<h1>Jump to Page - Open End Pagination (100 items total, lazy loaded)</h1>
<p>
Current: Page {currentPageIndex}, Max Known: {maxKnownPage}, Mode: {openEnd ? 'Open-End' : 'Closed'}
</p>
</div>
}
columnDefinitions={[
{ header: 'ID', cell: (item: Instance) => item.id },
{ header: 'State', cell: (item: Instance) => item.state },
{ header: 'Type', cell: (item: Instance) => item.type },
{ header: 'DNS Name', cell: (item: Instance) => item.dnsName || '-' },
]}
items={currentItems}
pagination={
<Pagination
ref={jumpToPageRef}
currentPageIndex={currentPageIndex}
pagesCount={maxKnownPage}
openEnd={openEnd}
onChange={({ detail }) => {
const requestedPage = detail.currentPageIndex;
// If page already loaded, just navigate
if (loadedPages[requestedPage]) {
setCurrentPageIndex(requestedPage);
return;
}
// Otherwise, load the page
setJumpToPageIsLoading(true);
loadPage(requestedPage)
.then(items => {
setLoadedPages(prev => ({ ...prev, [requestedPage]: items }));
setCurrentPageIndex(requestedPage);
setMaxKnownPage(Math.max(maxKnownPage, requestedPage));
setJumpToPageIsLoading(false);
})
.catch((error: { message: string; maxPage?: number }) => {
const newMaxPage = error.maxPage || maxKnownPage;
setMaxKnownPage(newMaxPage);
setOpenEnd(false);
jumpToPageRef.current?.setError(true);
// Load all pages from current to max
const pagesToLoad = [];
for (let i = 1; i <= newMaxPage; i++) {
if (!loadedPages[i]) {
pagesToLoad.push(loadPage(i).then(items => ({ page: i, items })));
}
}

Promise.all(pagesToLoad).then(results => {
setLoadedPages(prev => {
const updated = { ...prev };
results.forEach(({ page, items }) => {
updated[page] = items;
});
return updated;
});
setCurrentPageIndex(newMaxPage);
setJumpToPageIsLoading(false);
});
});
}}
onNextPageClick={({ detail }) => {
// If page already loaded, just navigate
if (loadedPages[detail.requestedPageIndex]) {
setCurrentPageIndex(detail.requestedPageIndex);
return;
}
// Load the next page
setJumpToPageIsLoading(true);
loadPage(detail.requestedPageIndex)
.then(items => {
setLoadedPages(prev => ({ ...prev, [detail.requestedPageIndex]: items }));
setCurrentPageIndex(detail.requestedPageIndex);
setMaxKnownPage(Math.max(maxKnownPage, detail.requestedPageIndex));
setJumpToPageIsLoading(false);
})
.catch((error: { message: string; maxPage?: number }) => {
// Discovered the end - switch to closed pagination and stay on current page
if (error.maxPage) {
setMaxKnownPage(error.maxPage);
setOpenEnd(false);
}
// Reset to current page (undo the navigation that already happened)
setCurrentPageIndex(currentPageIndex);
jumpToPageRef.current?.setError(true);
setJumpToPageIsLoading(false);
});
}}
jumpToPage={{
loading: jumpToPageIsLoading,
}}
/>
}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,8 @@ exports[`test-utils selectors 1`] = `
"pagination": [
"awsui_button-current_fvjdu",
"awsui_button_fvjdu",
"awsui_jump-to-page-input_fvjdu",
"awsui_jump-to-page_fvjdu",
"awsui_page-number_fvjdu",
"awsui_root_fvjdu",
],
Expand Down
33 changes: 24 additions & 9 deletions src/input/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface InternalInputProps
__inheritFormFieldProps?: boolean;
__injectAnalyticsComponentMetadata?: boolean;
__skipNativeAttributesWarnings?: SkipWarnings;
__inlineLabelText?: string;
}

function InternalInput(
Expand Down Expand Up @@ -93,6 +94,7 @@ function InternalInput(
__inheritFormFieldProps,
__injectAnalyticsComponentMetadata,
__skipNativeAttributesWarnings,
__inlineLabelText,
style,
...rest
}: InternalInputProps,
Expand Down Expand Up @@ -196,6 +198,18 @@ function InternalInput(
},
};

const renderMainInput = () => (
Copy link
Member

Choose a reason for hiding this comment

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

const renderMainInput = <WithNativeAttributes....

<WithNativeAttributes
{...attributes}
tag="input"
componentName="Input"
nativeAttributes={nativeInputAttributes}
skipWarnings={__skipNativeAttributesWarnings}
ref={mergedRef}
style={getInputStyles(style)}
/>
);

return (
<div
{...baseProps}
Expand All @@ -211,15 +225,16 @@ function InternalInput(
<InternalIcon name={__leftIcon} variant={disabled || readOnly ? 'disabled' : __leftIconVariant} />
</span>
)}
<WithNativeAttributes
{...attributes}
tag="input"
componentName="Input"
nativeAttributes={nativeInputAttributes}
skipWarnings={__skipNativeAttributesWarnings}
ref={mergedRef}
style={getInputStyles(style)}
/>
{__inlineLabelText ? (
<div className={styles['inline-label-wrapper']}>
<label htmlFor={controlId} className={styles['inline-label']}>
{__inlineLabelText}
</label>
<div className={styles['inline-label-trigger-wrapper']}>{renderMainInput()}</div>
</div>
) : (
renderMainInput()
)}
{__rightIcon && (
<span
className={styles['input-icon-right']}
Expand Down
32 changes: 32 additions & 0 deletions src/input/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,35 @@
.input-button-right {
/* used in test-utils */
}

$inlineLabel-border-radius: 2px;

.inline-label-trigger-wrapper {
margin-block-start: -7px;
}

.inline-label-wrapper {
margin-block-start: calc(awsui.$space-scaled-xs * -1);
}

.inline-label {
Copy link
Member

Choose a reason for hiding this comment

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

move this to mixin so it can be reused between select and input, probably move to https://github.com/cloudscape-design/components/blob/main/src/internal/styles/forms/mixins.scss

background: linear-gradient(to bottom, awsui.$color-background-layout-main, awsui.$color-background-input-default);
border-start-start-radius: $inlineLabel-border-radius;
border-start-end-radius: $inlineLabel-border-radius;
border-end-start-radius: $inlineLabel-border-radius;
border-end-end-radius: $inlineLabel-border-radius;
box-sizing: border-box;
display: inline-block;
color: awsui.$color-text-form-label;
font-weight: awsui.$font-display-label-weight;
font-size: awsui.$font-size-body-s;
line-height: 14px;
letter-spacing: awsui.$letter-spacing-body-s;
position: relative;
inset-inline-start: calc(awsui.$border-width-field + awsui.$space-field-horizontal - awsui.$space-scaled-xxs);
margin-block-start: awsui.$space-scaled-xs;
padding-block-end: 2px;
padding-inline: awsui.$space-scaled-xxs;
max-inline-size: calc(100% - (2 * awsui.$space-field-horizontal));
z-index: 1;
}
Loading
Loading