From bcf8871c54b73e2bb895ab1a2299ab5ea1544d64 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Mon, 5 Jan 2026 15:18:05 -0800 Subject: [PATCH 01/14] feat: implement pagination - jump to page --- .../test-utils-selectors.test.tsx.snap | 2 + src/pagination/__tests__/pagination.test.tsx | 210 +++++++++++++++++- src/pagination/index.tsx | 7 +- src/pagination/interfaces.ts | 29 ++- src/pagination/internal.tsx | 140 +++++++++++- src/pagination/styles.scss | 15 ++ src/test-utils/dom/pagination/index.ts | 26 +++ 7 files changed, 420 insertions(+), 9 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index 86add834db..9259c1ee27 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -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", ], diff --git a/src/pagination/__tests__/pagination.test.tsx b/src/pagination/__tests__/pagination.test.tsx index b32e6e13c3..5ae1bc1546 100644 --- a/src/pagination/__tests__/pagination.test.tsx +++ b/src/pagination/__tests__/pagination.test.tsx @@ -1,9 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import * as React from 'react'; -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; -import Pagination from '../../../lib/components/pagination'; +import Pagination, { PaginationProps } from '../../../lib/components/pagination'; import createWrapper, { PaginationWrapper } from '../../../lib/components/test-utils/dom'; const getItemsContent = (wrapper: PaginationWrapper) => @@ -301,3 +301,209 @@ describe('open-end pagination', () => { ); }); }); + +describe('jump to page', () => { + test('should render jump to page input and button when jumpToPage is provided', () => { + const { wrapper } = renderPagination(); + + expect(wrapper.findJumpToPageInput()).toBeTruthy(); + expect(wrapper.findJumpToPageButton()).toBeTruthy(); + }); + + test('should not render jump to page when jumpToPage is not provided', () => { + const { wrapper } = renderPagination(); + + expect(wrapper.findJumpToPageInput()).toBeNull(); + expect(wrapper.findJumpToPageButton()).toBeNull(); + }); + + test('should show loading state on jump to page button', () => { + const { wrapper } = renderPagination( + + ); + + expect(wrapper.findJumpToPageButton()!.findLoadingIndicator()).toBeTruthy(); + }); + + test('should disable jump to page button when input is empty', () => { + const { wrapper } = renderPagination(); + + wrapper.findJumpToPageInput()!.setInputValue(''); + expect(wrapper.findJumpToPageButton()!.getElement()).toBeDisabled(); + }); + + test('should disable jump to page button when input equals current page', () => { + const { wrapper } = renderPagination(); + + expect(wrapper.findJumpToPageButton()!.getElement()).toBeDisabled(); + }); + + test('should set min attribute to 1 on input', () => { + const { wrapper } = renderPagination(); + + expect(wrapper.findJumpToPageInput()!.findNativeInput().getElement()).toHaveAttribute('min', '1'); + }); + + test('should set max attribute to pagesCount in closed mode', () => { + const { wrapper } = renderPagination(); + + expect(wrapper.findJumpToPageInput()!.findNativeInput().getElement()).toHaveAttribute('max', '10'); + }); + + test('should not set max attribute in open-end mode', () => { + const { wrapper } = renderPagination( + + ); + + expect(wrapper.findJumpToPageInput()!.findNativeInput().getElement()).not.toHaveAttribute('max'); + }); + + describe('closed mode validation', () => { + test('should navigate to valid page in range', () => { + const onChange = jest.fn(); + const { wrapper } = renderPagination( + + ); + + wrapper.findJumpToPageInput()!.setInputValue('5'); + wrapper.findJumpToPageButton()!.click(); + + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + detail: { currentPageIndex: 5 }, + }) + ); + }); + + test('should navigate to first page when input is less than 1', () => { + const onChange = jest.fn(); + const { wrapper } = renderPagination( + + ); + + wrapper.findJumpToPageInput()!.setInputValue('0'); + wrapper.findJumpToPageButton()!.click(); + + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + detail: { currentPageIndex: 1 }, + }) + ); + expect(wrapper.findJumpToPageInput()!.findNativeInput().getElement()).toHaveValue(1); + }); + + test('should show error and navigate to last page when input exceeds pagesCount', () => { + const onChange = jest.fn(); + const { wrapper } = renderPagination( + + ); + + wrapper.findJumpToPageInput()!.setInputValue('15'); + wrapper.findJumpToPageButton()!.click(); + + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + detail: { currentPageIndex: 10 }, + }) + ); + expect(wrapper.findJumpToPageInput()!.findNativeInput().getElement()).toHaveValue(10); + }); + }); + + describe('open-end mode', () => { + test('should navigate to any page without validation', () => { + const onChange = jest.fn(); + const { wrapper } = renderPagination( + + ); + + wrapper.findJumpToPageInput()!.setInputValue('100'); + wrapper.findJumpToPageButton()!.click(); + + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + detail: { currentPageIndex: 100 }, + }) + ); + }); + }); + + describe('error handling via ref', () => { + test('should show error popover when setError is called', () => { + const ref = React.createRef(); + const { wrapper, rerender } = renderPagination( + + ); + + ref.current?.setError(true); + rerender(); + + // Error popover should be visible + expect(wrapper.findPopover()).not.toBeNull(); + }); + + test('should clear error when user types in input', () => { + const ref = React.createRef(); + const { wrapper, rerender } = renderPagination( + + ); + + ref.current?.setError(true); + rerender(); + + wrapper.findJumpToPageInput()!.setInputValue('5'); + + // Error should be cleared - popover should not be visible + expect(wrapper.findPopover()).toBeNull(); + }); + + test('should clear error when user navigates successfully', () => { + const ref = React.createRef(); + const onChange = jest.fn(); + const { wrapper, rerender } = renderPagination( + + ); + + ref.current?.setError(true); + rerender(); + + wrapper.findPageNumberByIndex(3)!.click(); + + expect(onChange).toHaveBeenCalled(); + // Error should be cleared + expect(wrapper.findPopover()).toBeNull(); + }); + }); + + describe('keyboard navigation', () => { + test('should submit on Enter key', () => { + const onChange = jest.fn(); + const { wrapper } = renderPagination( + + ); + + const input = wrapper.findJumpToPageInput()!.findNativeInput().getElement(); + wrapper.findJumpToPageInput()!.setInputValue('7'); + fireEvent.keyDown(input, { keyCode: 13, key: 'Enter' }); + + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + detail: { currentPageIndex: 7 }, + }) + ); + }); + + test('should not submit on Enter when input equals current page', () => { + const onChange = jest.fn(); + const { wrapper } = renderPagination( + + ); + + const input = wrapper.findJumpToPageInput()!.findNativeInput().getElement(); + wrapper.findJumpToPageInput()!.setInputValue('5'); + fireEvent.keyDown(input, { keyCode: 13, key: 'Enter' }); + + expect(onChange).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/pagination/index.tsx b/src/pagination/index.tsx index 4cd09564db..cdb47f3741 100644 --- a/src/pagination/index.tsx +++ b/src/pagination/index.tsx @@ -13,12 +13,13 @@ import InternalPagination from './internal'; export { PaginationProps }; -export default function Pagination(props: PaginationProps) { +const Pagination = React.forwardRef((props, ref) => { const baseComponentProps = useBaseComponent('Pagination', { props: { openEnd: props.openEnd } }); return ( ); -} +}); applyDisplayName(Pagination, 'Pagination'); + +export default Pagination; diff --git a/src/pagination/interfaces.ts b/src/pagination/interfaces.ts index 4b736a2061..6bfedcd423 100644 --- a/src/pagination/interfaces.ts +++ b/src/pagination/interfaces.ts @@ -48,6 +48,7 @@ export interface PaginationProps { * @i18n */ ariaLabels?: PaginationProps.Labels; + i18nStrings?: PaginationProps.I18nStrings; /** * Called when a user interaction causes a pagination change. The event `detail` contains the new `currentPageIndex`. @@ -68,6 +69,10 @@ export interface PaginationProps { * * `requestedPageIndex` (integer) - The index of the requested page. */ onNextPageClick?: NonCancelableEventHandler; + /** + * Jump to page configuration + */ + jumpToPage?: PaginationProps.JumpToPageProps; } export namespace PaginationProps { @@ -76,6 +81,16 @@ export namespace PaginationProps { paginationLabel?: string; previousPageLabel?: string; pageLabel?: (pageNumber: number) => string; + jumpToPageButton?: string; + } + + export interface I18nStrings { + jumpToPageError?: string; + jumpToPageLabel?: string; + } + + export interface ChangeDetail { + currentPageIndex: number; } export interface PageClickDetail { @@ -83,7 +98,17 @@ export namespace PaginationProps { requestedPageIndex: number; } - export interface ChangeDetail { - currentPageIndex: number; + export interface JumpToPageProps { + /** + * User controlled loading state when jump to page callback is executing + */ + loading?: boolean; + } + + export interface JumpToPageRef { + /** + * Set error state for jump to page. Component will auto-clear when user types or navigates. + */ + setError: (hasError: boolean) => void; } } diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index ec6ecad9b3..29ec6d1b46 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; +import React, { useState } from 'react'; import clsx from 'clsx'; import { @@ -8,12 +8,17 @@ import { getAnalyticsMetadataAttribute, } from '@cloudscape-design/component-toolkit/internal/analytics-metadata'; +import InternalButton from '../button/internal'; import { useInternalI18n } from '../i18n/context'; import InternalIcon from '../icon/internal'; +import { BaseChangeDetail } from '../input/interfaces'; +import InternalInput from '../input/internal'; import { getBaseProps } from '../internal/base-component'; import { useTableComponentsContext } from '../internal/context/table-component-context'; -import { fireNonCancelableEvent } from '../internal/events'; +import { fireNonCancelableEvent, NonCancelableCustomEvent } from '../internal/events'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; +import InternalPopover from '../popover/internal'; +import InternalSpaceBetween from '../space-between/internal'; import { GeneratedAnalyticsMetadataPaginationClick } from './analytics-metadata/interfaces'; import { PaginationProps } from './interfaces'; import { getPaginationState, range } from './utils'; @@ -24,9 +29,14 @@ const defaultAriaLabels: Required = { nextPageLabel: '', paginationLabel: '', previousPageLabel: '', + jumpToPageButton: '', pageLabel: pageNumber => `${pageNumber}`, }; +const defaultI18nStrings: Required = { + jumpToPageLabel: 'Page', + jumpToPageError: 'Page out of range. Showing last available page.', +}; interface PageButtonProps { className?: string; ariaLabel: string; @@ -99,24 +109,47 @@ function PageNumber({ pageIndex, ...rest }: PageButtonProps) { ); } -type InternalPaginationProps = PaginationProps & InternalBaseComponentProps; +type InternalPaginationProps = PaginationProps & + InternalBaseComponentProps & { + jumpToPageRef?: React.Ref; + }; export default function InternalPagination({ openEnd, currentPageIndex, ariaLabels, + i18nStrings, pagesCount, disabled, onChange, onNextPageClick, onPreviousPageClick, __internalRootRef, + jumpToPage, + jumpToPageRef, ...rest }: InternalPaginationProps) { const baseProps = getBaseProps(rest); const { leftDots, leftIndex, rightIndex, rightDots } = getPaginationState(currentPageIndex, pagesCount, openEnd); + const [jumpToPageValue, setJumpToPageValue] = useState(currentPageIndex?.toString()); + const prevLoadingRef = React.useRef(jumpToPage?.loading); + const [popoverVisible, setPopoverVisible] = useState(false); + const [hasError, setHasError] = useState(false); const i18n = useInternalI18n('pagination'); + // Expose setError function via ref + React.useImperativeHandle(jumpToPageRef, () => ({ + setError: (error: boolean) => setHasError(error), + })); + + // Sync input with currentPageIndex after loading completes + React.useEffect(() => { + if (prevLoadingRef.current && !jumpToPage?.loading) { + setJumpToPageValue(String(currentPageIndex)); + } + prevLoadingRef.current = jumpToPage?.loading; + }, [jumpToPage?.loading, currentPageIndex]); + const paginationLabel = ariaLabels?.paginationLabel; const nextPageLabel = i18n('ariaLabels.nextPageLabel', ariaLabels?.nextPageLabel) ?? defaultAriaLabels.nextPageLabel; const previousPageLabel = @@ -124,6 +157,9 @@ export default function InternalPagination({ const pageNumberLabelFn = i18n('ariaLabels.pageLabel', ariaLabels?.pageLabel, format => pageNumber => format({ pageNumber })) ?? defaultAriaLabels.pageLabel; + const jumpToPageLabel = i18nStrings?.jumpToPageLabel ?? defaultI18nStrings.jumpToPageLabel; + const jumpToPageButtonLabel = ariaLabels?.jumpToPageButton ?? defaultAriaLabels.jumpToPageButton; + const jumpToPageError = i18nStrings?.jumpToPageError ?? defaultI18nStrings.jumpToPageError; function handlePrevPageClick(requestedPageIndex: number) { handlePageClick(requestedPageIndex); @@ -142,9 +178,57 @@ export default function InternalPagination({ } function handlePageClick(requestedPageIndex: number) { + setJumpToPageValue(String(requestedPageIndex)); + setHasError(false); // Clear error on successful navigation fireNonCancelableEvent(onChange, { currentPageIndex: requestedPageIndex }); } + function handleJumpToPageClick(requestedPageIndex: number) { + if (requestedPageIndex < 1) { + // Out of range lower bound - navigate to first page + setJumpToPageValue('1'); + setHasError(false); + fireNonCancelableEvent(onChange, { currentPageIndex: 1 }); + return; + } + + if (openEnd) { + // Open-end: always navigate, parent will handle async loading + handlePageClick(requestedPageIndex); + } else { + // Closed-end: validate range + if (requestedPageIndex >= 1 && requestedPageIndex <= pagesCount) { + handlePageClick(requestedPageIndex); + } else { + // Out of range - set error and navigate to last page + setHasError(true); + setJumpToPageValue(String(pagesCount)); + fireNonCancelableEvent(onChange, { currentPageIndex: pagesCount }); + } + } + } + + // Auto-clear error when user types in the input + const handleInputChange = (e: NonCancelableCustomEvent) => { + setJumpToPageValue(e.detail.value); + if (hasError) { + setHasError(false); + } + }; + + // Show popover when error appears + React.useEffect(() => { + if (hasError) { + // For open-end, wait until loading completes + if (openEnd && jumpToPage?.loading) { + return; + } + setPopoverVisible(true); + } else { + setPopoverVisible(false); + } + }, [hasError, jumpToPage?.loading, openEnd]); + const previousButtonDisabled = disabled || currentPageIndex === 1; const nextButtonDisabled = disabled || (!openEnd && (pagesCount === 0 || currentPageIndex === pagesCount)); const tableComponentContext = useTableComponentsContext(); @@ -153,6 +237,20 @@ export default function InternalPagination({ tableComponentContext.paginationRef.current.totalPageCount = pagesCount; tableComponentContext.paginationRef.current.openEnd = openEnd; } + + const renderJumpToPageButton = () => { + return ( + handleJumpToPageClick(Number(jumpToPageValue))} + disabled={!jumpToPageValue || Number(jumpToPageValue) === currentPageIndex} + /> + ); + }; + return (
    + {jumpToPage && ( +
    + +
    + { + if (e.detail.keyCode === 13 && jumpToPageValue && Number(jumpToPageValue) !== currentPageIndex) { + handleJumpToPageClick(Number(jumpToPageValue)); + } + }} + /> +
    + {hasError ? ( + setPopoverVisible(detail.visible)} + > + {renderJumpToPageButton()} + + ) : ( + renderJumpToPageButton() + )} +
    +
    + )}
); } diff --git a/src/pagination/styles.scss b/src/pagination/styles.scss index 31095a9be2..576159e3df 100644 --- a/src/pagination/styles.scss +++ b/src/pagination/styles.scss @@ -13,6 +13,7 @@ flex-direction: row; flex-wrap: wrap; box-sizing: border-box; + align-items: center; //reset base styles for ul padding-inline-start: 0; margin-block: 0; @@ -78,6 +79,20 @@ } } +.jump-to-page { + border-inline-start: awsui.$border-divider-section-width solid awsui.$color-border-divider-default; + box-sizing: border-box; + margin-inline-start: awsui.$space-xs; + padding-inline-start: awsui.$space-xs; + padding-inline-start: 15px; + + &-input { + max-inline-size: 87px; + margin-block-start: -0.6em; + overflow: visible; + } +} + .dots { color: awsui.$color-text-interactive-default; } diff --git a/src/test-utils/dom/pagination/index.ts b/src/test-utils/dom/pagination/index.ts index cacf371836..00d7e119dc 100644 --- a/src/test-utils/dom/pagination/index.ts +++ b/src/test-utils/dom/pagination/index.ts @@ -2,6 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { ComponentWrapper, ElementWrapper, usesDom } from '@cloudscape-design/test-utils-core/dom'; +import ButtonWrapper from '../button'; +import InputWrapper from '../input'; +import PopoverWrapper from '../popover'; + import styles from '../../../pagination/styles.selectors.js'; export default class PaginationWrapper extends ComponentWrapper { @@ -34,6 +38,28 @@ export default class PaginationWrapper extends ComponentWrapper { return this.find(`li:last-child .${styles.button}`)!; } + /** + * Returns the jump to page input field. + */ + findJumpToPageInput(): InputWrapper | null { + return this.findComponent(`.${styles['jump-to-page-input']}`, InputWrapper); + } + + /** + * Returns the jump to page submit button. + */ + findJumpToPageButton(): ButtonWrapper | null { + const jumpToPageContainer = this.findByClassName(styles['jump-to-page']); + return jumpToPageContainer ? jumpToPageContainer.findComponent('button', ButtonWrapper) : null; + } + + /** + * Returns the error popover for jump to page. + */ + findPopover(): PopoverWrapper | null { + return this.findComponent(`.${PopoverWrapper.rootSelector}`, PopoverWrapper); + } + @usesDom isDisabled(): boolean { return this.element.classList.contains(styles['root-disabled']); From 695d8142d4477d529166b2ce3c50f15c29e18326 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Mon, 5 Jan 2026 15:35:29 -0800 Subject: [PATCH 02/14] feat: add input inline label and popover controlled visibility --- pages/table/jump-to-page-closed.page.tsx | 99 +++++++++++++++ pages/table/jump-to-page-open-end.page.tsx | 141 +++++++++++++++++++++ src/input/internal.tsx | 33 +++-- src/input/styles.scss | 32 +++++ src/popover/internal.tsx | 53 +++++--- 5 files changed, 333 insertions(+), 25 deletions(-) create mode 100644 pages/table/jump-to-page-closed.page.tsx create mode 100644 pages/table/jump-to-page-open-end.page.tsx diff --git a/pages/table/jump-to-page-closed.page.tsx b/pages/table/jump-to-page-closed.page.tsx new file mode 100644 index 0000000000..50542d7faa --- /dev/null +++ b/pages/table/jump-to-page-closed.page.tsx @@ -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 ( + Jump to Page - Closed Pagination (100 items, 10 pages)} + 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={ + + } + items={currentItems} + pagination={ + setCurrentPageIndex(detail.currentPageIndex)} + jumpToPage={{}} + /> + } + /> + ); +} diff --git a/pages/table/jump-to-page-open-end.page.tsx b/pages/table/jump-to-page-open-end.page.tsx new file mode 100644 index 0000000000..be7e91f2c4 --- /dev/null +++ b/pages/table/jump-to-page-open-end.page.tsx @@ -0,0 +1,141 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import Pagination 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>({ 1: generateItems(10) }); + const [jumpToPageError, setJumpToPageError] = useState(false); + const [jumpToPageIsLoading, setJumpToPageIsLoading] = useState(false); + const [maxKnownPage, setMaxKnownPage] = useState(1); + const [openEnd, setOpenEnd] = useState(true); + + const currentItems = loadedPages[currentPageIndex] || []; + + const loadPage = (pageIndex: number) => { + return new Promise((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 ( +
+

Jump to Page - Open End Pagination (100 items total, lazy loaded)

+

+ Current: Page {currentPageIndex}, Max Known: {maxKnownPage}, Mode: {openEnd ? 'Open-End' : 'Closed'} +

+ + } + 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={ + { + const requestedPage = detail.currentPageIndex; + // If page already loaded, just navigate + if (loadedPages[requestedPage]) { + setCurrentPageIndex(requestedPage); + setJumpToPageError(false); + return; + } + // Otherwise, load the page + setJumpToPageIsLoading(true); + loadPage(requestedPage) + .then(items => { + setLoadedPages(prev => ({ ...prev, [requestedPage]: items })); + setCurrentPageIndex(requestedPage); + setMaxKnownPage(Math.max(maxKnownPage, requestedPage)); + setJumpToPageError(false); + setJumpToPageIsLoading(false); + }) + .catch((error: { message: string; maxPage?: number }) => { + const newMaxPage = error.maxPage || maxKnownPage; + setMaxKnownPage(newMaxPage); + setOpenEnd(false); + setJumpToPageError(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); + setJumpToPageError(true); + setJumpToPageIsLoading(false); + }); + }} + jumpToPage={{ + isLoading: jumpToPageIsLoading, + hasError: jumpToPageError, + }} + /> + } + /> + ); +} diff --git a/src/input/internal.tsx b/src/input/internal.tsx index 7c798d294e..1cf90ff9dd 100644 --- a/src/input/internal.tsx +++ b/src/input/internal.tsx @@ -52,6 +52,7 @@ export interface InternalInputProps __inheritFormFieldProps?: boolean; __injectAnalyticsComponentMetadata?: boolean; __skipNativeAttributesWarnings?: SkipWarnings; + __inlineLabelText?: string; } function InternalInput( @@ -93,6 +94,7 @@ function InternalInput( __inheritFormFieldProps, __injectAnalyticsComponentMetadata, __skipNativeAttributesWarnings, + __inlineLabelText, style, ...rest }: InternalInputProps, @@ -196,6 +198,18 @@ function InternalInput( }, }; + const renderMainInput = () => ( + + ); + return (
)} - + {__inlineLabelText ? ( +
+ +
{renderMainInput()}
+
+ ) : ( + renderMainInput() + )} {__rightIcon && ( ; } export default React.forwardRef(InternalPopover); @@ -53,6 +55,8 @@ function InternalPopover( __onOpen, __internalRootRef, __closeAnalyticsAction, + visible: controlledVisible, + onVisibleChange, ...restProps }: InternalPopoverProps, @@ -65,7 +69,20 @@ function InternalPopover( const i18n = useInternalI18n('popover'); const dismissAriaLabel = i18n('dismissAriaLabel', restProps.dismissAriaLabel); - const [visible, setVisible] = useState(false); + const [internalVisible, setInternalVisible] = useState(false); + const isControlled = controlledVisible !== undefined; + const visible = isControlled ? controlledVisible : internalVisible; + + const updateVisible = useCallback( + (newVisible: boolean) => { + if (isControlled) { + fireNonCancelableEvent(onVisibleChange, { visible: newVisible }); + } else { + setInternalVisible(newVisible); + } + }, + [isControlled, onVisibleChange] + ); const focusTrigger = useCallback(() => { if (['text', 'text-inline'].includes(triggerType)) { @@ -77,13 +94,13 @@ function InternalPopover( const onTriggerClick = useCallback(() => { fireNonCancelableEvent(__onOpen); - setVisible(true); - }, [__onOpen]); + updateVisible(true); + }, [__onOpen, updateVisible]); const onDismiss = useCallback(() => { - setVisible(false); + updateVisible(false); focusTrigger(); - }, [focusTrigger]); + }, [focusTrigger, updateVisible]); const onTriggerKeyDown = useCallback( (event: React.KeyboardEvent) => { @@ -93,21 +110,25 @@ function InternalPopover( event.stopPropagation(); } if (isTabKey || isEscapeKey) { - setVisible(false); + updateVisible(false); } }, - [visible] + [visible, updateVisible] ); - useImperativeHandle(ref, () => ({ - dismiss: () => { - setVisible(false); - }, - focus: () => { - setVisible(false); - focusTrigger(); - }, - })); + useImperativeHandle( + ref, + () => ({ + dismiss: () => { + updateVisible(false); + }, + focus: () => { + updateVisible(false); + focusTrigger(); + }, + }), + [updateVisible, focusTrigger] + ); const clickFrameId = useRef(null); useEffect(() => { From d82c06af67bf0d2008e38e2febc36b2b54a4bda0 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Mon, 5 Jan 2026 16:21:15 -0800 Subject: [PATCH 03/14] fix: update demo pages to use ref-based error handling --- pages/pagination/permutations.page.tsx | 1 + pages/table/jump-to-page-open-end.page.tsx | 16 +++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pages/pagination/permutations.page.tsx b/pages/pagination/permutations.page.tsx index 6a8db3b507..7a9b711c10 100644 --- a/pages/pagination/permutations.page.tsx +++ b/pages/pagination/permutations.page.tsx @@ -26,6 +26,7 @@ const permutations = createPermutations([ pagesCount: [15], openEnd: [true, false], ariaLabels: [paginationLabels], + jumpToPage: [undefined, { loading: false }, { loading: true }], }, ]); diff --git a/pages/table/jump-to-page-open-end.page.tsx b/pages/table/jump-to-page-open-end.page.tsx index be7e91f2c4..9d86ecae14 100644 --- a/pages/table/jump-to-page-open-end.page.tsx +++ b/pages/table/jump-to-page-open-end.page.tsx @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; -import Pagination from '~components/pagination'; +import Pagination, { PaginationProps } from '~components/pagination'; import Table from '~components/table'; import { generateItems, Instance } from './generate-data'; @@ -13,10 +13,10 @@ const TOTAL_ITEMS = 100; // Simulated server-side total export default function JumpToPageOpenEndExample() { const [currentPageIndex, setCurrentPageIndex] = useState(1); const [loadedPages, setLoadedPages] = useState>({ 1: generateItems(10) }); - const [jumpToPageError, setJumpToPageError] = useState(false); const [jumpToPageIsLoading, setJumpToPageIsLoading] = useState(false); const [maxKnownPage, setMaxKnownPage] = useState(1); const [openEnd, setOpenEnd] = useState(true); + const jumpToPageRef = useRef(null); const currentItems = loadedPages[currentPageIndex] || []; @@ -56,6 +56,7 @@ export default function JumpToPageOpenEndExample() { items={currentItems} pagination={ ({ ...prev, [requestedPage]: items })); setCurrentPageIndex(requestedPage); setMaxKnownPage(Math.max(maxKnownPage, requestedPage)); - setJumpToPageError(false); setJumpToPageIsLoading(false); }) .catch((error: { message: string; maxPage?: number }) => { const newMaxPage = error.maxPage || maxKnownPage; setMaxKnownPage(newMaxPage); setOpenEnd(false); - setJumpToPageError(true); + jumpToPageRef.current?.setError(true); // Load all pages from current to max const pagesToLoad = []; for (let i = 1; i <= newMaxPage; i++) { @@ -126,13 +125,12 @@ export default function JumpToPageOpenEndExample() { } // Reset to current page (undo the navigation that already happened) setCurrentPageIndex(currentPageIndex); - setJumpToPageError(true); + jumpToPageRef.current?.setError(true); setJumpToPageIsLoading(false); }); }} jumpToPage={{ - isLoading: jumpToPageIsLoading, - hasError: jumpToPageError, + loading: jumpToPageIsLoading, }} /> } From c365516ff917e7df89e906c7708363b7c2047e12 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Tue, 6 Jan 2026 16:27:23 +0100 Subject: [PATCH 04/14] fix: Fixes drag handle UAP buttons to never render outside viewport (#4155) --- src/internal/components/drag-handle-wrapper/styles.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/internal/components/drag-handle-wrapper/styles.scss b/src/internal/components/drag-handle-wrapper/styles.scss index 0461aa84d4..b903138437 100644 --- a/src/internal/components/drag-handle-wrapper/styles.scss +++ b/src/internal/components/drag-handle-wrapper/styles.scss @@ -89,16 +89,16 @@ $direction-button-offset: awsui.$space-static-xxs; inset-block-start: calc(-4 * $direction-button-wrapper-size); } .direction-button-wrapper-forced-bottom-0 { - inset-block-start: calc(1 * $direction-button-wrapper-size); + inset-block-start: calc(1 * $direction-button-wrapper-size - 50%); } .direction-button-wrapper-forced-bottom-1 { - inset-block-start: calc(2 * $direction-button-wrapper-size); + inset-block-start: calc(2 * $direction-button-wrapper-size - 50%); } .direction-button-wrapper-forced-bottom-2 { - inset-block-start: calc(3 * $direction-button-wrapper-size); + inset-block-start: calc(3 * $direction-button-wrapper-size - 50%); } .direction-button-wrapper-forced-bottom-3 { - inset-block-start: calc(4 * $direction-button-wrapper-size); + inset-block-start: calc(4 * $direction-button-wrapper-size - 50%); } .direction-button { From 19a989dc4e6f63c4701041be5aab795a27328607 Mon Sep 17 00:00:00 2001 From: Amanuel Abiy Date: Tue, 6 Jan 2026 16:53:04 +0100 Subject: [PATCH 05/14] chore: switch build-tools to transition branch (#4138) --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 336ecde7f4..bd7240abfb 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@babel/core": "^7.23.7", "@babel/plugin-syntax-typescript": "^7.23.3", "@cloudscape-design/browser-test-tools": "^3.0.0", - "@cloudscape-design/build-tools": "github:cloudscape-design/build-tools#main", + "@cloudscape-design/build-tools": "github:cloudscape-design/build-tools#transition-main-do-not-edit", "@cloudscape-design/documenter": "^1.0.0", "@cloudscape-design/global-styles": "^1.0.0", "@cloudscape-design/jest-preset": "^2.0.0", @@ -111,7 +111,7 @@ "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.2", "loader-utils": "^3.2.1", - "lodash": "^4.17.23", + "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.4.4", "mississippi": "^4.0.0", "mockdate": "^3.0.5", @@ -129,7 +129,7 @@ "rollup-plugin-license": "^3.0.1", "sass": "^1.89.2", "sass-loader": "^12.3.0", - "size-limit": "^12.0.0", + "size-limit": "^11.1.6", "stylelint": "^16.6.1", "stylelint-config-recommended-scss": "^14.0.0", "stylelint-no-unsupported-browser-features": "^8.0.2", @@ -163,7 +163,7 @@ "stylelint --fix" ], "package-lock.json": [ - "prepare-package-lock" + "node ./scripts/unlock-package-lock.js" ] }, "size-limit": [ From a674d82144d1c662ac0fd5167a56af310250f2b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:21:23 +0100 Subject: [PATCH 06/14] chore: Bump qs and express (#4145) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrei Zhaleznichenka --- package-lock.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index 380c4b26b7..d7f5fc5c64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9979,6 +9979,22 @@ "dev": true, "license": "MIT" }, + "node_modules/express/node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/extend": { "version": "3.0.2", "dev": true, From 8643ecc0221df3c19ee182c5537fa9479946b7fe Mon Sep 17 00:00:00 2001 From: Amanuel Abiy Date: Wed, 7 Jan 2026 16:54:01 +0100 Subject: [PATCH 07/14] chore: update build-tools dependency to use main branch (#4162) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bd7240abfb..bca070986c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@babel/core": "^7.23.7", "@babel/plugin-syntax-typescript": "^7.23.3", "@cloudscape-design/browser-test-tools": "^3.0.0", - "@cloudscape-design/build-tools": "github:cloudscape-design/build-tools#transition-main-do-not-edit", + "@cloudscape-design/build-tools": "github:cloudscape-design/build-tools#main", "@cloudscape-design/documenter": "^1.0.0", "@cloudscape-design/global-styles": "^1.0.0", "@cloudscape-design/jest-preset": "^2.0.0", From 783743a448bd5027b6b97de8538eee0348299e07 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Wed, 7 Jan 2026 17:35:01 +0100 Subject: [PATCH 08/14] fix: Fixes UAP buttons forced padding (#4171) --- src/internal/components/drag-handle-wrapper/styles.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/internal/components/drag-handle-wrapper/styles.scss b/src/internal/components/drag-handle-wrapper/styles.scss index b903138437..0461aa84d4 100644 --- a/src/internal/components/drag-handle-wrapper/styles.scss +++ b/src/internal/components/drag-handle-wrapper/styles.scss @@ -89,16 +89,16 @@ $direction-button-offset: awsui.$space-static-xxs; inset-block-start: calc(-4 * $direction-button-wrapper-size); } .direction-button-wrapper-forced-bottom-0 { - inset-block-start: calc(1 * $direction-button-wrapper-size - 50%); + inset-block-start: calc(1 * $direction-button-wrapper-size); } .direction-button-wrapper-forced-bottom-1 { - inset-block-start: calc(2 * $direction-button-wrapper-size - 50%); + inset-block-start: calc(2 * $direction-button-wrapper-size); } .direction-button-wrapper-forced-bottom-2 { - inset-block-start: calc(3 * $direction-button-wrapper-size - 50%); + inset-block-start: calc(3 * $direction-button-wrapper-size); } .direction-button-wrapper-forced-bottom-3 { - inset-block-start: calc(4 * $direction-button-wrapper-size - 50%); + inset-block-start: calc(4 * $direction-button-wrapper-size); } .direction-button { From e7d87d6d36aabcdf58f4bf7928b50e89e6100711 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Fri, 9 Jan 2026 16:20:27 -0800 Subject: [PATCH 09/14] feat: add pagination - jump to page, update i18n, snapshots --- pages/pagination/permutations.page.tsx | 6 +- pages/table/jump-to-page-closed.page.tsx | 12 +- pages/table/jump-to-page-open-end.page.tsx | 14 +- .../__snapshots__/documenter.test.ts.snap | 426 ++++++++++------- src/i18n/messages-types.ts | 17 +- src/i18n/messages/all.en.json | 6 + src/input/internal.tsx | 6 +- src/input/styles.scss | 27 +- src/internal/styles/forms/mixins.scss | 36 ++ src/pagination/__tests__/pagination.test.tsx | 12 +- src/pagination/index.tsx | 4 +- src/pagination/interfaces.ts | 2 +- src/pagination/internal.tsx | 434 +++++++++--------- src/popover/internal.tsx | 4 +- src/select/parts/styles.scss | 28 +- src/test-utils/dom/pagination/index.ts | 2 +- 16 files changed, 592 insertions(+), 444 deletions(-) diff --git a/pages/pagination/permutations.page.tsx b/pages/pagination/permutations.page.tsx index 7a9b711c10..16bdb3ecdb 100644 --- a/pages/pagination/permutations.page.tsx +++ b/pages/pagination/permutations.page.tsx @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import React from 'react'; +import I18nProvider from '~components/i18n'; +import messages from '~components/i18n/messages/all.en'; import Pagination, { PaginationProps } from '~components/pagination'; import createPermutations from '../utils/permutations'; @@ -32,11 +34,11 @@ const permutations = createPermutations([ export default function PaginationPermutations() { return ( - <> +

Pagination permutations

} /> - +
); } diff --git a/pages/table/jump-to-page-closed.page.tsx b/pages/table/jump-to-page-closed.page.tsx index 50542d7faa..e05ac153d6 100644 --- a/pages/table/jump-to-page-closed.page.tsx +++ b/pages/table/jump-to-page-closed.page.tsx @@ -3,6 +3,8 @@ import React, { useState } from 'react'; import { CollectionPreferences } from '~components'; +import I18nProvider from '~components/i18n'; +import messages from '~components/i18n/messages/all.en'; import Pagination from '~components/pagination'; import Table from '~components/table'; @@ -11,7 +13,7 @@ import { generateItems, Instance } from './generate-data'; const allItems = generateItems(100); const PAGE_SIZE = 10; -export default function JumpToPageClosedExample() { +function JumpToPageClosedContent() { const [currentPageIndex, setCurrentPageIndex] = useState(1); const totalPages = Math.ceil(allItems.length / PAGE_SIZE); @@ -97,3 +99,11 @@ export default function JumpToPageClosedExample() { /> ); } + +export default function JumpToPageClosedExample() { + return ( + + + + ); +} diff --git a/pages/table/jump-to-page-open-end.page.tsx b/pages/table/jump-to-page-open-end.page.tsx index 9d86ecae14..0798a10d55 100644 --- a/pages/table/jump-to-page-open-end.page.tsx +++ b/pages/table/jump-to-page-open-end.page.tsx @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useRef, useState } from 'react'; +import I18nProvider from '~components/i18n'; +import messages from '~components/i18n/messages/all.en'; import Pagination, { PaginationProps } from '~components/pagination'; import Table from '~components/table'; @@ -10,13 +12,13 @@ import { generateItems, Instance } from './generate-data'; const PAGE_SIZE = 10; const TOTAL_ITEMS = 100; // Simulated server-side total -export default function JumpToPageOpenEndExample() { +function JumpToPageOpenEndContent() { const [currentPageIndex, setCurrentPageIndex] = useState(1); const [loadedPages, setLoadedPages] = useState>({ 1: generateItems(10) }); const [jumpToPageIsLoading, setJumpToPageIsLoading] = useState(false); const [maxKnownPage, setMaxKnownPage] = useState(1); const [openEnd, setOpenEnd] = useState(true); - const jumpToPageRef = useRef(null); + const jumpToPageRef = useRef(null); const currentItems = loadedPages[currentPageIndex] || []; @@ -137,3 +139,11 @@ export default function JumpToPageOpenEndExample() { /> ); } + +export default function JumpToPageOpenEndExample() { + return ( + + + + ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index f31f0c5bca..fb7432d9fc 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -18259,7 +18259,19 @@ exports[`Components definition for pagination matches the snapshot: pagination 1 "name": "onPreviousPageClick", }, ], - "functions": [], + "functions": [ + { + "description": "Set error state for jump to page. Component will auto-clear when user types or navigates.", + "name": "setError", + "parameters": [ + { + "name": "hasError", + "type": "boolean", + }, + ], + "returnType": "void", + }, + ], "name": "Pagination", "properties": [ { @@ -18283,6 +18295,11 @@ Example: "inlineType": { "name": "PaginationProps.Labels", "properties": [ + { + "name": "jumpToPageButton", + "optional": true, + "type": "string", + }, { "name": "nextPageLabel", "optional": true, @@ -18334,6 +18351,44 @@ from changing page before items are loaded.", "optional": true, "type": "boolean", }, + { + "inlineType": { + "name": "PaginationProps.I18nStrings", + "properties": [ + { + "name": "jumpToPageError", + "optional": true, + "type": "string", + }, + { + "name": "jumpToPageLabel", + "optional": true, + "type": "string", + }, + ], + "type": "object", + }, + "name": "i18nStrings", + "optional": true, + "type": "PaginationProps.I18nStrings", + }, + { + "description": "Jump to page configuration", + "inlineType": { + "name": "PaginationProps.JumpToPageProps", + "properties": [ + { + "name": "loading", + "optional": true, + "type": "boolean", + }, + ], + "type": "object", + }, + "name": "jumpToPage", + "optional": true, + "type": "PaginationProps.JumpToPageProps", + }, { "description": "Sets the pagination variant. It can be either default (when setting it to \`false\`) or open ended (when setting it to \`true\`). Default pagination navigates you through the items list. The open-end variant enables you @@ -22365,6 +22420,11 @@ The function will be called when a user clicks on the trigger button.", "inlineType": { "name": "PaginationProps.Labels", "properties": [ + { + "name": "jumpToPageButton", + "optional": true, + "type": "string", + }, { "name": "nextPageLabel", "optional": true, @@ -34625,6 +34685,33 @@ Returns the current value of the input.", ], }, }, + { + "description": "Returns the jump to page submit button.", + "name": "findJumpToPageButton", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ButtonWrapper", + }, + }, + { + "description": "Returns the jump to page input field.", + "name": "findJumpToPageInput", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "InputWrapper", + }, + }, + { + "description": "Returns the error popover for jump to page.", + "name": "findJumpToPagePopover", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "PopoverWrapper", + }, + }, { "name": "findNextPageButton", "parameters": [], @@ -34698,6 +34785,88 @@ Returns the current value of the input.", ], "name": "PaginationWrapper", }, + { + "methods": [ + { + "name": "findContent", + "parameters": [ + { + "defaultValue": "{ renderWithPortal: false }", + "description": "* renderWithPortal (boolean) - Flag to find the content when the popover is rendered with a portal", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ renderWithPortal: boolean; }", + }, + ], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "name": "findDismissButton", + "parameters": [ + { + "defaultValue": "{ renderWithPortal: false }", + "description": "* renderWithPortal (boolean) - Flag to find the dismiss button when the popover is rendered with a portal", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ renderWithPortal: boolean; }", + }, + ], + "returnType": { + "isNullable": true, + "name": "ButtonWrapper", + }, + }, + { + "name": "findHeader", + "parameters": [ + { + "defaultValue": "{ renderWithPortal: false }", + "description": "* renderWithPortal (boolean) - Flag to find the header when the popover is rendered with a portal", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ renderWithPortal: boolean; }", + }, + ], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "name": "findTrigger", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + ], + "name": "PopoverWrapper", + }, { "methods": [ { @@ -38921,88 +39090,6 @@ Returns null if the panel layout is not resizable.", ], "name": "PieChartWrapper", }, - { - "methods": [ - { - "name": "findContent", - "parameters": [ - { - "defaultValue": "{ renderWithPortal: false }", - "description": "* renderWithPortal (boolean) - Flag to find the content when the popover is rendered with a portal", - "flags": { - "isOptional": false, - }, - "name": "options", - "typeName": "{ renderWithPortal: boolean; }", - }, - ], - "returnType": { - "isNullable": true, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - { - "name": "findDismissButton", - "parameters": [ - { - "defaultValue": "{ renderWithPortal: false }", - "description": "* renderWithPortal (boolean) - Flag to find the dismiss button when the popover is rendered with a portal", - "flags": { - "isOptional": false, - }, - "name": "options", - "typeName": "{ renderWithPortal: boolean; }", - }, - ], - "returnType": { - "isNullable": true, - "name": "ButtonWrapper", - }, - }, - { - "name": "findHeader", - "parameters": [ - { - "defaultValue": "{ renderWithPortal: false }", - "description": "* renderWithPortal (boolean) - Flag to find the header when the popover is rendered with a portal", - "flags": { - "isOptional": false, - }, - "name": "options", - "typeName": "{ renderWithPortal: boolean; }", - }, - ], - "returnType": { - "isNullable": true, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - { - "name": "findTrigger", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - ], - "name": "PopoverWrapper", - }, { "methods": [ { @@ -45157,6 +45244,33 @@ is '2-indexed', that is, the first section in a card has an index of \`2\`.", "name": "ElementWrapper", }, }, + { + "description": "Returns the jump to page submit button.", + "name": "findJumpToPageButton", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ButtonWrapper", + }, + }, + { + "description": "Returns the jump to page input field.", + "name": "findJumpToPageInput", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "InputWrapper", + }, + }, + { + "description": "Returns the error popover for jump to page.", + "name": "findJumpToPagePopover", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "PopoverWrapper", + }, + }, { "name": "findNextPageButton", "parameters": [], @@ -45207,6 +45321,79 @@ is '2-indexed', that is, the first section in a card has an index of \`2\`.", ], "name": "PaginationWrapper", }, + { + "methods": [ + { + "name": "findContent", + "parameters": [ + { + "defaultValue": "{ + renderWithPortal: false + }", + "description": "* renderWithPortal (boolean) - Flag to find the content when the popover is rendered with a portal", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ renderWithPortal: boolean; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "name": "findDismissButton", + "parameters": [ + { + "defaultValue": "{ + renderWithPortal: false + }", + "description": "* renderWithPortal (boolean) - Flag to find the dismiss button when the popover is rendered with a portal", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ renderWithPortal: boolean; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "ButtonWrapper", + }, + }, + { + "name": "findHeader", + "parameters": [ + { + "defaultValue": "{ + renderWithPortal: false + }", + "description": "* renderWithPortal (boolean) - Flag to find the header when the popover is rendered with a portal", + "flags": { + "isOptional": false, + }, + "name": "options", + "typeName": "{ renderWithPortal: boolean; }", + }, + ], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "name": "findTrigger", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + ], + "name": "PopoverWrapper", + }, { "methods": [ { @@ -48162,79 +48349,6 @@ Returns null if the panel layout is not resizable.", ], "name": "PieChartWrapper", }, - { - "methods": [ - { - "name": "findContent", - "parameters": [ - { - "defaultValue": "{ - renderWithPortal: false - }", - "description": "* renderWithPortal (boolean) - Flag to find the content when the popover is rendered with a portal", - "flags": { - "isOptional": false, - }, - "name": "options", - "typeName": "{ renderWithPortal: boolean; }", - }, - ], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - { - "name": "findDismissButton", - "parameters": [ - { - "defaultValue": "{ - renderWithPortal: false - }", - "description": "* renderWithPortal (boolean) - Flag to find the dismiss button when the popover is rendered with a portal", - "flags": { - "isOptional": false, - }, - "name": "options", - "typeName": "{ renderWithPortal: boolean; }", - }, - ], - "returnType": { - "isNullable": false, - "name": "ButtonWrapper", - }, - }, - { - "name": "findHeader", - "parameters": [ - { - "defaultValue": "{ - renderWithPortal: false - }", - "description": "* renderWithPortal (boolean) - Flag to find the header when the popover is rendered with a portal", - "flags": { - "isOptional": false, - }, - "name": "options", - "typeName": "{ renderWithPortal: boolean; }", - }, - ], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - { - "name": "findTrigger", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - ], - "name": "PopoverWrapper", - }, { "methods": [ { diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 6964a13e85..28bb167d3e 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -196,6 +196,8 @@ export interface I18nFormatArgTypes { "i18nStrings.endMonthLabel": never; "i18nStrings.endDateLabel": never; "i18nStrings.endTimeLabel": never; + "i18nStrings.datePlaceholder": never; + "i18nStrings.timePlaceholder": never; "i18nStrings.dateTimeConstraintText": never; "i18nStrings.dateConstraintText": never; "i18nStrings.slashedDateTimeConstraintText": never; @@ -223,12 +225,15 @@ export interface I18nFormatArgTypes { "i18nStrings.loadingText": never; } "error-boundary": { - "i18nStrings.headerText"?: never; - "i18nStrings.descriptionText"?: { - hasFeedback: boolean; - Feedback: (chunks: React.ReactNode[]) => React.ReactNode; - }; - "i18nStrings.refreshActionText"?: never; + "i18nStrings.headerText": never; + "i18nStrings.descriptionText": { + "hasFeedback": string; + } + "i18nStrings.refreshActionText": never; + } + "features-notification-drawer": { + "i18nStrings.title": never; + "i18nStrings.viewAll": never; } "file-token-group": { "i18nStrings.limitShowFewer": never; diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 7e88102e91..9585251aa1 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -158,6 +158,8 @@ "i18nStrings.endMonthLabel": "End month", "i18nStrings.endDateLabel": "End date", "i18nStrings.endTimeLabel": "End time", + "i18nStrings.datePlaceholder": "YYYY-MM-DD", + "i18nStrings.timePlaceholder": "hh:mm:ss", "i18nStrings.dateTimeConstraintText": "For date, use YYYY/MM/DD. For time, use 24 hr format.", "i18nStrings.dateConstraintText": "For date, use YYYY/MM/DD.", "i18nStrings.slashedDateTimeConstraintText": "For date, use YYYY/MM/DD. For time, use 24 hr format.", @@ -180,6 +182,10 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Refresh to try again. We are tracking this issue, but you can share more information here.} other {Refresh to try again.}}", "i18nStrings.refreshActionText": "Refresh page" }, + "features-notification-drawer": { + "i18nStrings.title": "Latest feature releases", + "i18nStrings.viewAll": "View all feature releases" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Show fewer", "i18nStrings.limitShowMore": "Show more", diff --git a/src/input/internal.tsx b/src/input/internal.tsx index 1cf90ff9dd..26a90bbe60 100644 --- a/src/input/internal.tsx +++ b/src/input/internal.tsx @@ -198,7 +198,7 @@ function InternalInput( }, }; - const renderMainInput = () => ( + const mainInput = ( {__inlineLabelText} -
{renderMainInput()}
+
{mainInput}
) : ( - renderMainInput() + mainInput )} {__rightIcon && ( { describe('error handling via ref', () => { test('should show error popover when setError is called', () => { - const ref = React.createRef(); + const ref = React.createRef(); const { wrapper, rerender } = renderPagination( ); @@ -439,11 +439,11 @@ describe('jump to page', () => { rerender(); // Error popover should be visible - expect(wrapper.findPopover()).not.toBeNull(); + expect(wrapper.findJumpToPagePopover()).not.toBeNull(); }); test('should clear error when user types in input', () => { - const ref = React.createRef(); + const ref = React.createRef(); const { wrapper, rerender } = renderPagination( ); @@ -454,11 +454,11 @@ describe('jump to page', () => { wrapper.findJumpToPageInput()!.setInputValue('5'); // Error should be cleared - popover should not be visible - expect(wrapper.findPopover()).toBeNull(); + expect(wrapper.findJumpToPagePopover()).toBeNull(); }); test('should clear error when user navigates successfully', () => { - const ref = React.createRef(); + const ref = React.createRef(); const onChange = jest.fn(); const { wrapper, rerender } = renderPagination( @@ -471,7 +471,7 @@ describe('jump to page', () => { expect(onChange).toHaveBeenCalled(); // Error should be cleared - expect(wrapper.findPopover()).toBeNull(); + expect(wrapper.findJumpToPagePopover()).toBeNull(); }); }); diff --git a/src/pagination/index.tsx b/src/pagination/index.tsx index cdb47f3741..2ef99ab558 100644 --- a/src/pagination/index.tsx +++ b/src/pagination/index.tsx @@ -13,13 +13,13 @@ import InternalPagination from './internal'; export { PaginationProps }; -const Pagination = React.forwardRef((props, ref) => { +const Pagination = React.forwardRef((props, ref) => { const baseComponentProps = useBaseComponent('Pagination', { props: { openEnd: props.openEnd } }); return ( = { }; const defaultI18nStrings: Required = { - jumpToPageLabel: 'Page', - jumpToPageError: 'Page out of range. Showing last available page.', + jumpToPageLabel: '', + jumpToPageError: '', }; + interface PageButtonProps { className?: string; ariaLabel: string; @@ -109,137 +110,134 @@ function PageNumber({ pageIndex, ...rest }: PageButtonProps) { ); } -type InternalPaginationProps = PaginationProps & - InternalBaseComponentProps & { - jumpToPageRef?: React.Ref; - }; -export default function InternalPagination({ - openEnd, - currentPageIndex, - ariaLabels, - i18nStrings, - pagesCount, - disabled, - onChange, - onNextPageClick, - onPreviousPageClick, - __internalRootRef, - jumpToPage, - jumpToPageRef, - ...rest -}: InternalPaginationProps) { - const baseProps = getBaseProps(rest); - const { leftDots, leftIndex, rightIndex, rightDots } = getPaginationState(currentPageIndex, pagesCount, openEnd); - const [jumpToPageValue, setJumpToPageValue] = useState(currentPageIndex?.toString()); - const prevLoadingRef = React.useRef(jumpToPage?.loading); - const [popoverVisible, setPopoverVisible] = useState(false); - const [hasError, setHasError] = useState(false); +type InternalPaginationProps = PaginationProps & InternalBaseComponentProps; - const i18n = useInternalI18n('pagination'); +const InternalPagination = React.forwardRef( + ( + { + openEnd, + currentPageIndex, + ariaLabels, + i18nStrings, + pagesCount, + disabled, + onChange, + onNextPageClick, + onPreviousPageClick, + __internalRootRef, + jumpToPage, + ...rest + }: InternalPaginationProps, + ref: React.Ref + ) => { + const baseProps = getBaseProps(rest); + const { leftDots, leftIndex, rightIndex, rightDots } = getPaginationState(currentPageIndex, pagesCount, openEnd); + const [jumpToPageValue, setJumpToPageValue] = useState(currentPageIndex?.toString()); + const prevLoadingRef = React.useRef(jumpToPage?.loading); + const [popoverVisible, setPopoverVisible] = useState(false); + const [hasError, setHasError] = useState(false); - // Expose setError function via ref - React.useImperativeHandle(jumpToPageRef, () => ({ - setError: (error: boolean) => setHasError(error), - })); + const i18n = useInternalI18n('pagination'); - // Sync input with currentPageIndex after loading completes - React.useEffect(() => { - if (prevLoadingRef.current && !jumpToPage?.loading) { - setJumpToPageValue(String(currentPageIndex)); - } - prevLoadingRef.current = jumpToPage?.loading; - }, [jumpToPage?.loading, currentPageIndex]); + // Expose setError function via ref + React.useImperativeHandle(ref, () => ({ + setError: (error: boolean) => setHasError(error), + })); - const paginationLabel = ariaLabels?.paginationLabel; - const nextPageLabel = i18n('ariaLabels.nextPageLabel', ariaLabels?.nextPageLabel) ?? defaultAriaLabels.nextPageLabel; - const previousPageLabel = - i18n('ariaLabels.previousPageLabel', ariaLabels?.previousPageLabel) ?? defaultAriaLabels.previousPageLabel; - const pageNumberLabelFn = - i18n('ariaLabels.pageLabel', ariaLabels?.pageLabel, format => pageNumber => format({ pageNumber })) ?? - defaultAriaLabels.pageLabel; - const jumpToPageLabel = i18nStrings?.jumpToPageLabel ?? defaultI18nStrings.jumpToPageLabel; - const jumpToPageButtonLabel = ariaLabels?.jumpToPageButton ?? defaultAriaLabels.jumpToPageButton; - const jumpToPageError = i18nStrings?.jumpToPageError ?? defaultI18nStrings.jumpToPageError; + // Sync input with currentPageIndex after loading completes + React.useEffect(() => { + if (prevLoadingRef.current && !jumpToPage?.loading) { + setJumpToPageValue(String(currentPageIndex)); + } + prevLoadingRef.current = jumpToPage?.loading; + }, [jumpToPage?.loading, currentPageIndex]); - function handlePrevPageClick(requestedPageIndex: number) { - handlePageClick(requestedPageIndex); - fireNonCancelableEvent(onPreviousPageClick, { - requestedPageAvailable: true, - requestedPageIndex: requestedPageIndex, - }); - } + const paginationLabel = ariaLabels?.paginationLabel; + const nextPageLabel = i18n('ariaLabels.nextPageLabel', ariaLabels?.nextPageLabel); + const previousPageLabel = i18n('ariaLabels.previousPageLabel', ariaLabels?.previousPageLabel); + const pageNumberLabelFn = + i18n('ariaLabels.pageLabel', ariaLabels?.pageLabel, format => pageNumber => format({ pageNumber })) ?? + defaultAriaLabels.pageLabel; + const jumpToPageLabel = + i18n('i18nStrings.jumpToPageInputLabel', i18nStrings?.jumpToPageLabel) ?? defaultI18nStrings.jumpToPageLabel; + const jumpToPageButtonLabel = + i18n('ariaLabels.jumpToPageButtonLabel', ariaLabels?.jumpToPageButton) ?? defaultAriaLabels.jumpToPageButton; + const jumpToPageError = + i18n('i18nStrings.jumpToPageError', i18nStrings?.jumpToPageError) ?? defaultI18nStrings.jumpToPageError; - function handleNextPageClick(requestedPageIndex: number) { - handlePageClick(requestedPageIndex); - fireNonCancelableEvent(onNextPageClick, { - requestedPageAvailable: currentPageIndex < pagesCount, - requestedPageIndex: requestedPageIndex, - }); - } + function handlePrevPageClick(requestedPageIndex: number) { + handlePageClick(requestedPageIndex); + fireNonCancelableEvent(onPreviousPageClick, { + requestedPageAvailable: true, + requestedPageIndex: requestedPageIndex, + }); + } - function handlePageClick(requestedPageIndex: number) { - setJumpToPageValue(String(requestedPageIndex)); - setHasError(false); // Clear error on successful navigation - fireNonCancelableEvent(onChange, { currentPageIndex: requestedPageIndex }); - } + function handleNextPageClick(requestedPageIndex: number) { + handlePageClick(requestedPageIndex); + fireNonCancelableEvent(onNextPageClick, { + requestedPageAvailable: currentPageIndex < pagesCount, + requestedPageIndex: requestedPageIndex, + }); + } - function handleJumpToPageClick(requestedPageIndex: number) { - if (requestedPageIndex < 1) { - // Out of range lower bound - navigate to first page - setJumpToPageValue('1'); - setHasError(false); - fireNonCancelableEvent(onChange, { currentPageIndex: 1 }); - return; + function handlePageClick(requestedPageIndex: number, errorState?: boolean) { + setJumpToPageValue(String(requestedPageIndex)); + setHasError(!!errorState); // Clear error on successful navigation + fireNonCancelableEvent(onChange, { currentPageIndex: requestedPageIndex }); } - if (openEnd) { - // Open-end: always navigate, parent will handle async loading - handlePageClick(requestedPageIndex); - } else { - // Closed-end: validate range - if (requestedPageIndex >= 1 && requestedPageIndex <= pagesCount) { + function handleJumpToPageClick(requestedPageIndex: number) { + if (requestedPageIndex < 1) { + handlePageClick(1); + return; + } + + if (openEnd) { + // Open-end: always navigate, parent will handle async loading handlePageClick(requestedPageIndex); } else { - // Out of range - set error and navigate to last page - setHasError(true); - setJumpToPageValue(String(pagesCount)); - fireNonCancelableEvent(onChange, { currentPageIndex: pagesCount }); + // Closed-end: validate range + if (requestedPageIndex >= 1 && requestedPageIndex <= pagesCount) { + handlePageClick(requestedPageIndex); + } else { + // Out of range - set error and navigate to last page + handlePageClick(pagesCount, true); + } } } - } - // Auto-clear error when user types in the input - const handleInputChange = (e: NonCancelableCustomEvent) => { - setJumpToPageValue(e.detail.value); - if (hasError) { - setHasError(false); - } - }; + // Auto-clear error when user types in the input + const handleInputChange = (e: NonCancelableCustomEvent) => { + setJumpToPageValue(e.detail.value); + if (hasError) { + setHasError(false); + } + }; - // Show popover when error appears - React.useEffect(() => { - if (hasError) { - // For open-end, wait until loading completes - if (openEnd && jumpToPage?.loading) { - return; + // Show popover when error appears + React.useEffect(() => { + if (hasError) { + // For open-end, wait until loading completes + if (openEnd && jumpToPage?.loading) { + return; + } + setPopoverVisible(true); + } else { + setPopoverVisible(false); } - setPopoverVisible(true); - } else { - setPopoverVisible(false); - } - }, [hasError, jumpToPage?.loading, openEnd]); + }, [hasError, jumpToPage?.loading, openEnd]); - const previousButtonDisabled = disabled || currentPageIndex === 1; - const nextButtonDisabled = disabled || (!openEnd && (pagesCount === 0 || currentPageIndex === pagesCount)); - const tableComponentContext = useTableComponentsContext(); - if (tableComponentContext?.paginationRef?.current) { - tableComponentContext.paginationRef.current.currentPageIndex = currentPageIndex; - tableComponentContext.paginationRef.current.totalPageCount = pagesCount; - tableComponentContext.paginationRef.current.openEnd = openEnd; - } + const previousButtonDisabled = disabled || currentPageIndex === 1; + const nextButtonDisabled = disabled || (!openEnd && (pagesCount === 0 || currentPageIndex === pagesCount)); + const tableComponentContext = useTableComponentsContext(); + if (tableComponentContext?.paginationRef?.current) { + tableComponentContext.paginationRef.current.currentPageIndex = currentPageIndex; + tableComponentContext.paginationRef.current.totalPageCount = pagesCount; + tableComponentContext.paginationRef.current.openEnd = openEnd; + } - const renderJumpToPageButton = () => { - return ( + const jumpToPageButton = ( ); - }; - return ( -
    - - - - - {leftDots &&
  • ...
  • } - {range(leftIndex, rightIndex).map(pageIndex => ( - - ))} - {rightDots &&
  • ...
  • } - {!openEnd && pagesCount > 1 && ( + + + - )} - - - - {jumpToPage && ( -
    - -
    - { - if (e.detail.keyCode === 13 && jumpToPageValue && Number(jumpToPageValue) !== currentPageIndex) { - handleJumpToPageClick(Number(jumpToPageValue)); - } - }} - /> -
    - {hasError ? ( - setPopoverVisible(detail.visible)} - > - {renderJumpToPageButton()} - - ) : ( - renderJumpToPageButton() - )} -
    -
    - )} -
- ); -} + {leftDots &&
  • ...
  • } + {range(leftIndex, rightIndex).map(pageIndex => ( + + ))} + {rightDots &&
  • ...
  • } + {!openEnd && pagesCount > 1 && ( + + )} + + + + {jumpToPage && ( +
    + +
    + { + if (e.detail.keyCode === 13 && jumpToPageValue && Number(jumpToPageValue) !== currentPageIndex) { + handleJumpToPageClick(Number(jumpToPageValue)); + } + }} + /> +
    + {hasError ? ( + setPopoverVisible(detail.visible)} + > + {jumpToPageButton} + + ) : ( + jumpToPageButton + )} +
    +
    + )} + + ); + } +); + +export default InternalPagination; diff --git a/src/popover/internal.tsx b/src/popover/internal.tsx index 64d8f9e8d9..f290579848 100644 --- a/src/popover/internal.tsx +++ b/src/popover/internal.tsx @@ -140,7 +140,7 @@ function InternalPopover( const onDocumentClick = () => { // Dismiss popover unless there was a click inside within the last animation frame. if (clickFrameId.current === null) { - setVisible(false); + updateVisible(false); } }; @@ -149,7 +149,7 @@ function InternalPopover( return () => { document.removeEventListener('mousedown', onDocumentClick); }; - }, []); + }, [updateVisible]); const popoverClasses = usePortalModeClasses(triggerRef, { resetVisualContext: true }); diff --git a/src/select/parts/styles.scss b/src/select/parts/styles.scss index 805b3344bd..8257df5bb1 100644 --- a/src/select/parts/styles.scss +++ b/src/select/parts/styles.scss @@ -5,6 +5,7 @@ @use '../../internal/styles' as styles; @use '../../internal/styles/tokens' as awsui; +@use '../../internal/styles/forms/mixins' as forms; @use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible; .placeholder { @@ -12,7 +13,6 @@ } $checkbox-size: awsui.$size-control; -$inlineLabel-border-radius: 2px; .item { display: flex; @@ -100,35 +100,17 @@ $inlineLabel-border-radius: 2px; } .inline-label-trigger-wrapper { - margin-block-start: -7px; + @include forms.inline-label-trigger-wrapper; } .inline-label-wrapper { - margin-block-start: calc(awsui.$space-scaled-xs * -1); + @include forms.inline-label-wrapper; } .inline-label { - 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; + @include forms.inline-label; &-disabled { - background: linear-gradient(to bottom, awsui.$color-background-layout-main, awsui.$color-background-input-disabled); + @include forms.inline-label-disabled; } } diff --git a/src/test-utils/dom/pagination/index.ts b/src/test-utils/dom/pagination/index.ts index 00d7e119dc..58416472ae 100644 --- a/src/test-utils/dom/pagination/index.ts +++ b/src/test-utils/dom/pagination/index.ts @@ -56,7 +56,7 @@ export default class PaginationWrapper extends ComponentWrapper { /** * Returns the error popover for jump to page. */ - findPopover(): PopoverWrapper | null { + findJumpToPagePopover(): PopoverWrapper | null { return this.findComponent(`.${PopoverWrapper.rootSelector}`, PopoverWrapper); } From ac8b1801e0a35576e3f57405b59add67121b6b5a Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Thu, 15 Jan 2026 15:42:41 -0800 Subject: [PATCH 10/14] Remove unrelated i18n changes --- src/i18n/messages-types.ts | 20 ++++++-------------- src/i18n/messages/all.ar.json | 5 +---- src/i18n/messages/all.de.json | 5 +---- src/i18n/messages/all.en-GB.json | 5 +---- src/i18n/messages/all.en.json | 13 ++----------- src/i18n/messages/all.es.json | 5 +---- src/i18n/messages/all.fr.json | 5 +---- src/i18n/messages/all.id.json | 5 +---- src/i18n/messages/all.it.json | 5 +---- src/i18n/messages/all.ja.json | 5 +---- src/i18n/messages/all.ko.json | 5 +---- src/i18n/messages/all.pt-BR.json | 5 +---- src/i18n/messages/all.tr.json | 5 +---- src/i18n/messages/all.zh-CN.json | 5 +---- src/i18n/messages/all.zh-TW.json | 5 +---- 15 files changed, 21 insertions(+), 77 deletions(-) diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 28bb167d3e..a3a0f8caf2 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -196,8 +196,6 @@ export interface I18nFormatArgTypes { "i18nStrings.endMonthLabel": never; "i18nStrings.endDateLabel": never; "i18nStrings.endTimeLabel": never; - "i18nStrings.datePlaceholder": never; - "i18nStrings.timePlaceholder": never; "i18nStrings.dateTimeConstraintText": never; "i18nStrings.dateConstraintText": never; "i18nStrings.slashedDateTimeConstraintText": never; @@ -225,15 +223,12 @@ export interface I18nFormatArgTypes { "i18nStrings.loadingText": never; } "error-boundary": { - "i18nStrings.headerText": never; - "i18nStrings.descriptionText": { - "hasFeedback": string; - } - "i18nStrings.refreshActionText": never; - } - "features-notification-drawer": { - "i18nStrings.title": never; - "i18nStrings.viewAll": never; + "i18nStrings.headerText"?: never; + "i18nStrings.descriptionText"?: { + hasFeedback: boolean; + Feedback: (chunks: React.ReactNode[]) => React.ReactNode; + }; + "i18nStrings.refreshActionText"?: never; } "file-token-group": { "i18nStrings.limitShowFewer": never; @@ -320,9 +315,6 @@ export interface I18nFormatArgTypes { "pageNumber": string | number; } "ariaLabels.previousPageLabel": never; - "ariaLabels.jumpToPageButtonLabel": never; - "i18nStrings.jumpToPageInputLabel": never; - "i18nStrings.jumpToPageError": never; } "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": never; diff --git a/src/i18n/messages/all.ar.json b/src/i18n/messages/all.ar.json index 6e6ba3d443..008e420b13 100644 --- a/src/i18n/messages/all.ar.json +++ b/src/i18n/messages/all.ar.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "الصفحة التالية", "ariaLabels.pageLabel": "صفحة رقم {pageNumber} من إجمالي عدد الصفحات", - "ariaLabels.previousPageLabel": "الصفحة السابقة", - "ariaLabels.jumpToPageButtonLabel": "الانتقال إلى الصفحة", - "i18nStrings.jumpToPageInputLabel": "صفحة", - "i18nStrings.jumpToPageError": "الصفحة خارج نطاق الوصول. جارٍ عرض آخر صفحة متاحة." + "ariaLabels.previousPageLabel": "الصفحة السابقة" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "مقبض تغيير حجم اللوحة", diff --git a/src/i18n/messages/all.de.json b/src/i18n/messages/all.de.json index b23c65fb69..f7a9cef13a 100644 --- a/src/i18n/messages/all.de.json +++ b/src/i18n/messages/all.de.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "Nächste Seite", "ariaLabels.pageLabel": "Seite {pageNumber} aller Seiten", - "ariaLabels.previousPageLabel": "Vorherige Seite", - "ariaLabels.jumpToPageButtonLabel": "Zur Seite springen", - "i18nStrings.jumpToPageInputLabel": "Seite", - "i18nStrings.jumpToPageError": "Seite ist außerhalb des Bereichs. Zeigt die letzte verfügbare Seite an." + "ariaLabels.previousPageLabel": "Vorherige Seite" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Größe des Panels ändern", diff --git a/src/i18n/messages/all.en-GB.json b/src/i18n/messages/all.en-GB.json index 6339a55961..1ad2c1996e 100644 --- a/src/i18n/messages/all.en-GB.json +++ b/src/i18n/messages/all.en-GB.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "Next page", "ariaLabels.pageLabel": "Page {pageNumber} of all pages", - "ariaLabels.previousPageLabel": "Previous page", - "ariaLabels.jumpToPageButtonLabel": "Jump to page", - "i18nStrings.jumpToPageInputLabel": "Page", - "i18nStrings.jumpToPageError": "Page out of range. Showing the last available page." + "ariaLabels.previousPageLabel": "Previous page" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Panel resize handle", diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 9585251aa1..b8f988720f 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -158,8 +158,6 @@ "i18nStrings.endMonthLabel": "End month", "i18nStrings.endDateLabel": "End date", "i18nStrings.endTimeLabel": "End time", - "i18nStrings.datePlaceholder": "YYYY-MM-DD", - "i18nStrings.timePlaceholder": "hh:mm:ss", "i18nStrings.dateTimeConstraintText": "For date, use YYYY/MM/DD. For time, use 24 hr format.", "i18nStrings.dateConstraintText": "For date, use YYYY/MM/DD.", "i18nStrings.slashedDateTimeConstraintText": "For date, use YYYY/MM/DD. For time, use 24 hr format.", @@ -182,10 +180,6 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Refresh to try again. We are tracking this issue, but you can share more information here.} other {Refresh to try again.}}", "i18nStrings.refreshActionText": "Refresh page" }, - "features-notification-drawer": { - "i18nStrings.title": "Latest feature releases", - "i18nStrings.viewAll": "View all feature releases" - }, "file-token-group": { "i18nStrings.limitShowFewer": "Show fewer", "i18nStrings.limitShowMore": "Show more", @@ -246,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "Next page", "ariaLabels.pageLabel": "Page {pageNumber} of all pages", - "ariaLabels.previousPageLabel": "Previous page", - "ariaLabels.jumpToPageButtonLabel": "Jump to page", - "i18nStrings.jumpToPageInputLabel": "Page", - "i18nStrings.jumpToPageError": "Page out of range. Showing last available page." + "ariaLabels.previousPageLabel": "Previous page" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Panel resize handle", @@ -476,4 +467,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Loading next step", "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" } -} \ No newline at end of file +} diff --git a/src/i18n/messages/all.es.json b/src/i18n/messages/all.es.json index 9e5e5ba3c9..90c8f35248 100644 --- a/src/i18n/messages/all.es.json +++ b/src/i18n/messages/all.es.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "Página siguiente", "ariaLabels.pageLabel": "Página {pageNumber} de todas las páginas", - "ariaLabels.previousPageLabel": "Página anterior", - "ariaLabels.jumpToPageButtonLabel": "Ir a la página", - "i18nStrings.jumpToPageInputLabel": "Página", - "i18nStrings.jumpToPageError": "Página fuera de rango. Se muestra la última página disponible." + "ariaLabels.previousPageLabel": "Página anterior" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Controlador de cambio del tamaño del panel", diff --git a/src/i18n/messages/all.fr.json b/src/i18n/messages/all.fr.json index cc467d6942..6a41faff14 100644 --- a/src/i18n/messages/all.fr.json +++ b/src/i18n/messages/all.fr.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "Page suivante", "ariaLabels.pageLabel": "Page {pageNumber} de toutes les pages", - "ariaLabels.previousPageLabel": "Page précédente", - "ariaLabels.jumpToPageButtonLabel": "Aller à la page", - "i18nStrings.jumpToPageInputLabel": "Page", - "i18nStrings.jumpToPageError": "Page hors de portée. Affichage de la dernière page disponible." + "ariaLabels.previousPageLabel": "Page précédente" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Poignée de redimensionnement du panneau", diff --git a/src/i18n/messages/all.id.json b/src/i18n/messages/all.id.json index 04300d28a4..c34b7988d1 100644 --- a/src/i18n/messages/all.id.json +++ b/src/i18n/messages/all.id.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "Halaman berikutnya", "ariaLabels.pageLabel": "Halaman {pageNumber} dari semua halaman", - "ariaLabels.previousPageLabel": "Halaman sebelumnya", - "ariaLabels.jumpToPageButtonLabel": "Langsung ke halaman", - "i18nStrings.jumpToPageInputLabel": "Halaman", - "i18nStrings.jumpToPageError": "Halaman di luar rentang. Menampilkan halaman terakhir yang tersedia." + "ariaLabels.previousPageLabel": "Halaman sebelumnya" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Handel pengubahan ukuran panel", diff --git a/src/i18n/messages/all.it.json b/src/i18n/messages/all.it.json index 59a260d0bf..a6515a5e71 100644 --- a/src/i18n/messages/all.it.json +++ b/src/i18n/messages/all.it.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "Pagina successiva", "ariaLabels.pageLabel": "Pagina {pageNumber} di tutte le pagine", - "ariaLabels.previousPageLabel": "Pagina precedente", - "ariaLabels.jumpToPageButtonLabel": "Vai alla pagina", - "i18nStrings.jumpToPageInputLabel": "Pagina", - "i18nStrings.jumpToPageError": "Pagina fuori intervallo. Mostra l'ultima pagina disponibile." + "ariaLabels.previousPageLabel": "Pagina precedente" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Maniglia di ridimensionamento del pannello", diff --git a/src/i18n/messages/all.ja.json b/src/i18n/messages/all.ja.json index 5b3a9a84a2..65d9f9c68d 100644 --- a/src/i18n/messages/all.ja.json +++ b/src/i18n/messages/all.ja.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "次のページ", "ariaLabels.pageLabel": "全ページ中 {pageNumber} ページ", - "ariaLabels.previousPageLabel": "前のページ", - "ariaLabels.jumpToPageButtonLabel": "ページに移動", - "i18nStrings.jumpToPageInputLabel": "ページ", - "i18nStrings.jumpToPageError": "ページが範囲外です。最後に利用可能なページを表示しています。" + "ariaLabels.previousPageLabel": "前のページ" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "パネルのサイズ変更ハンドル", diff --git a/src/i18n/messages/all.ko.json b/src/i18n/messages/all.ko.json index 3843034e7e..4e125fa177 100644 --- a/src/i18n/messages/all.ko.json +++ b/src/i18n/messages/all.ko.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "다음 페이지", "ariaLabels.pageLabel": "전체 페이지 중 {pageNumber}페이지", - "ariaLabels.previousPageLabel": "이전 페이지", - "ariaLabels.jumpToPageButtonLabel": "페이지로 이동", - "i18nStrings.jumpToPageInputLabel": "페이지", - "i18nStrings.jumpToPageError": "페이지가 범위를 벗어났습니다. 마지막으로 사용 가능한 페이지를 표시합니다." + "ariaLabels.previousPageLabel": "이전 페이지" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "패널 크기 조정 핸들", diff --git a/src/i18n/messages/all.pt-BR.json b/src/i18n/messages/all.pt-BR.json index f873eb481b..050ae67cb7 100644 --- a/src/i18n/messages/all.pt-BR.json +++ b/src/i18n/messages/all.pt-BR.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "Próxima página", "ariaLabels.pageLabel": "Página {pageNumber} de todas as páginas", - "ariaLabels.previousPageLabel": "Página anterior", - "ariaLabels.jumpToPageButtonLabel": "Ir para a página", - "i18nStrings.jumpToPageInputLabel": "Página", - "i18nStrings.jumpToPageError": "Página fora do alcance. Mostrando a última página disponível." + "ariaLabels.previousPageLabel": "Página anterior" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Alça de redimensionamento do painel", diff --git a/src/i18n/messages/all.tr.json b/src/i18n/messages/all.tr.json index 8d7cf0dc8b..7fb8a438bc 100644 --- a/src/i18n/messages/all.tr.json +++ b/src/i18n/messages/all.tr.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "Sonraki sayfa", "ariaLabels.pageLabel": "Sayfa {pageNumber}/tüm sayfalar", - "ariaLabels.previousPageLabel": "Önceki sayfa", - "ariaLabels.jumpToPageButtonLabel": "Sayfaya atla", - "i18nStrings.jumpToPageInputLabel": "Sayfa", - "i18nStrings.jumpToPageError": "Aralık dışı sayfa. Mevcut son sayfa gösteriliyor." + "ariaLabels.previousPageLabel": "Önceki sayfa" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Panel yeniden boyutlandırma tutamacı", diff --git a/src/i18n/messages/all.zh-CN.json b/src/i18n/messages/all.zh-CN.json index 13c8b83a19..a1a50aec6a 100644 --- a/src/i18n/messages/all.zh-CN.json +++ b/src/i18n/messages/all.zh-CN.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "下一页", "ariaLabels.pageLabel": "所有页面中的第 {pageNumber} 页", - "ariaLabels.previousPageLabel": "上一页", - "ariaLabels.jumpToPageButtonLabel": "跳转到页面", - "i18nStrings.jumpToPageInputLabel": "页面", - "i18nStrings.jumpToPageError": "页面超出范围。显示最后一个可用页面。" + "ariaLabels.previousPageLabel": "上一页" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "面板大小调整手柄", diff --git a/src/i18n/messages/all.zh-TW.json b/src/i18n/messages/all.zh-TW.json index 9e8de5b797..a3e229d170 100644 --- a/src/i18n/messages/all.zh-TW.json +++ b/src/i18n/messages/all.zh-TW.json @@ -240,10 +240,7 @@ "pagination": { "ariaLabels.nextPageLabel": "下一頁", "ariaLabels.pageLabel": "所有頁面中的第 {pageNumber} 頁", - "ariaLabels.previousPageLabel": "上一頁", - "ariaLabels.jumpToPageButtonLabel": "跳到頁面", - "i18nStrings.jumpToPageInputLabel": "頁面", - "i18nStrings.jumpToPageError": "頁面超出範圍。顯示最後一個可用頁面。" + "ariaLabels.previousPageLabel": "上一頁" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "面板調整大小控點", From bc96e41f8f63ebb8f402f73a95f7a3767cb5564f Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Thu, 15 Jan 2026 15:46:42 -0800 Subject: [PATCH 11/14] Remove unrelated i18n changes --- src/i18n/messages-types.ts | 3 +++ src/i18n/messages/all.ar.json | 5 ++++- src/i18n/messages/all.de.json | 5 ++++- src/i18n/messages/all.en-GB.json | 5 ++++- src/i18n/messages/all.en.json | 7 +++++-- src/i18n/messages/all.es.json | 5 ++++- src/i18n/messages/all.fr.json | 5 ++++- src/i18n/messages/all.id.json | 5 ++++- src/i18n/messages/all.it.json | 5 ++++- src/i18n/messages/all.ja.json | 5 ++++- src/i18n/messages/all.ko.json | 5 ++++- src/i18n/messages/all.pt-BR.json | 5 ++++- src/i18n/messages/all.tr.json | 5 ++++- src/i18n/messages/all.zh-CN.json | 5 ++++- src/i18n/messages/all.zh-TW.json | 5 ++++- 15 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index a3a0f8caf2..6964a13e85 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -315,6 +315,9 @@ export interface I18nFormatArgTypes { "pageNumber": string | number; } "ariaLabels.previousPageLabel": never; + "ariaLabels.jumpToPageButtonLabel": never; + "i18nStrings.jumpToPageInputLabel": never; + "i18nStrings.jumpToPageError": never; } "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": never; diff --git a/src/i18n/messages/all.ar.json b/src/i18n/messages/all.ar.json index 008e420b13..6e6ba3d443 100644 --- a/src/i18n/messages/all.ar.json +++ b/src/i18n/messages/all.ar.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "الصفحة التالية", "ariaLabels.pageLabel": "صفحة رقم {pageNumber} من إجمالي عدد الصفحات", - "ariaLabels.previousPageLabel": "الصفحة السابقة" + "ariaLabels.previousPageLabel": "الصفحة السابقة", + "ariaLabels.jumpToPageButtonLabel": "الانتقال إلى الصفحة", + "i18nStrings.jumpToPageInputLabel": "صفحة", + "i18nStrings.jumpToPageError": "الصفحة خارج نطاق الوصول. جارٍ عرض آخر صفحة متاحة." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "مقبض تغيير حجم اللوحة", diff --git a/src/i18n/messages/all.de.json b/src/i18n/messages/all.de.json index f7a9cef13a..b23c65fb69 100644 --- a/src/i18n/messages/all.de.json +++ b/src/i18n/messages/all.de.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "Nächste Seite", "ariaLabels.pageLabel": "Seite {pageNumber} aller Seiten", - "ariaLabels.previousPageLabel": "Vorherige Seite" + "ariaLabels.previousPageLabel": "Vorherige Seite", + "ariaLabels.jumpToPageButtonLabel": "Zur Seite springen", + "i18nStrings.jumpToPageInputLabel": "Seite", + "i18nStrings.jumpToPageError": "Seite ist außerhalb des Bereichs. Zeigt die letzte verfügbare Seite an." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Größe des Panels ändern", diff --git a/src/i18n/messages/all.en-GB.json b/src/i18n/messages/all.en-GB.json index 1ad2c1996e..6339a55961 100644 --- a/src/i18n/messages/all.en-GB.json +++ b/src/i18n/messages/all.en-GB.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "Next page", "ariaLabels.pageLabel": "Page {pageNumber} of all pages", - "ariaLabels.previousPageLabel": "Previous page" + "ariaLabels.previousPageLabel": "Previous page", + "ariaLabels.jumpToPageButtonLabel": "Jump to page", + "i18nStrings.jumpToPageInputLabel": "Page", + "i18nStrings.jumpToPageError": "Page out of range. Showing the last available page." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Panel resize handle", diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index b8f988720f..7e88102e91 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "Next page", "ariaLabels.pageLabel": "Page {pageNumber} of all pages", - "ariaLabels.previousPageLabel": "Previous page" + "ariaLabels.previousPageLabel": "Previous page", + "ariaLabels.jumpToPageButtonLabel": "Jump to page", + "i18nStrings.jumpToPageInputLabel": "Page", + "i18nStrings.jumpToPageError": "Page out of range. Showing last available page." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Panel resize handle", @@ -467,4 +470,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Loading next step", "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.es.json b/src/i18n/messages/all.es.json index 90c8f35248..9e5e5ba3c9 100644 --- a/src/i18n/messages/all.es.json +++ b/src/i18n/messages/all.es.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "Página siguiente", "ariaLabels.pageLabel": "Página {pageNumber} de todas las páginas", - "ariaLabels.previousPageLabel": "Página anterior" + "ariaLabels.previousPageLabel": "Página anterior", + "ariaLabels.jumpToPageButtonLabel": "Ir a la página", + "i18nStrings.jumpToPageInputLabel": "Página", + "i18nStrings.jumpToPageError": "Página fuera de rango. Se muestra la última página disponible." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Controlador de cambio del tamaño del panel", diff --git a/src/i18n/messages/all.fr.json b/src/i18n/messages/all.fr.json index 6a41faff14..cc467d6942 100644 --- a/src/i18n/messages/all.fr.json +++ b/src/i18n/messages/all.fr.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "Page suivante", "ariaLabels.pageLabel": "Page {pageNumber} de toutes les pages", - "ariaLabels.previousPageLabel": "Page précédente" + "ariaLabels.previousPageLabel": "Page précédente", + "ariaLabels.jumpToPageButtonLabel": "Aller à la page", + "i18nStrings.jumpToPageInputLabel": "Page", + "i18nStrings.jumpToPageError": "Page hors de portée. Affichage de la dernière page disponible." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Poignée de redimensionnement du panneau", diff --git a/src/i18n/messages/all.id.json b/src/i18n/messages/all.id.json index c34b7988d1..04300d28a4 100644 --- a/src/i18n/messages/all.id.json +++ b/src/i18n/messages/all.id.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "Halaman berikutnya", "ariaLabels.pageLabel": "Halaman {pageNumber} dari semua halaman", - "ariaLabels.previousPageLabel": "Halaman sebelumnya" + "ariaLabels.previousPageLabel": "Halaman sebelumnya", + "ariaLabels.jumpToPageButtonLabel": "Langsung ke halaman", + "i18nStrings.jumpToPageInputLabel": "Halaman", + "i18nStrings.jumpToPageError": "Halaman di luar rentang. Menampilkan halaman terakhir yang tersedia." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Handel pengubahan ukuran panel", diff --git a/src/i18n/messages/all.it.json b/src/i18n/messages/all.it.json index a6515a5e71..59a260d0bf 100644 --- a/src/i18n/messages/all.it.json +++ b/src/i18n/messages/all.it.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "Pagina successiva", "ariaLabels.pageLabel": "Pagina {pageNumber} di tutte le pagine", - "ariaLabels.previousPageLabel": "Pagina precedente" + "ariaLabels.previousPageLabel": "Pagina precedente", + "ariaLabels.jumpToPageButtonLabel": "Vai alla pagina", + "i18nStrings.jumpToPageInputLabel": "Pagina", + "i18nStrings.jumpToPageError": "Pagina fuori intervallo. Mostra l'ultima pagina disponibile." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Maniglia di ridimensionamento del pannello", diff --git a/src/i18n/messages/all.ja.json b/src/i18n/messages/all.ja.json index 65d9f9c68d..5b3a9a84a2 100644 --- a/src/i18n/messages/all.ja.json +++ b/src/i18n/messages/all.ja.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "次のページ", "ariaLabels.pageLabel": "全ページ中 {pageNumber} ページ", - "ariaLabels.previousPageLabel": "前のページ" + "ariaLabels.previousPageLabel": "前のページ", + "ariaLabels.jumpToPageButtonLabel": "ページに移動", + "i18nStrings.jumpToPageInputLabel": "ページ", + "i18nStrings.jumpToPageError": "ページが範囲外です。最後に利用可能なページを表示しています。" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "パネルのサイズ変更ハンドル", diff --git a/src/i18n/messages/all.ko.json b/src/i18n/messages/all.ko.json index 4e125fa177..3843034e7e 100644 --- a/src/i18n/messages/all.ko.json +++ b/src/i18n/messages/all.ko.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "다음 페이지", "ariaLabels.pageLabel": "전체 페이지 중 {pageNumber}페이지", - "ariaLabels.previousPageLabel": "이전 페이지" + "ariaLabels.previousPageLabel": "이전 페이지", + "ariaLabels.jumpToPageButtonLabel": "페이지로 이동", + "i18nStrings.jumpToPageInputLabel": "페이지", + "i18nStrings.jumpToPageError": "페이지가 범위를 벗어났습니다. 마지막으로 사용 가능한 페이지를 표시합니다." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "패널 크기 조정 핸들", diff --git a/src/i18n/messages/all.pt-BR.json b/src/i18n/messages/all.pt-BR.json index 050ae67cb7..f873eb481b 100644 --- a/src/i18n/messages/all.pt-BR.json +++ b/src/i18n/messages/all.pt-BR.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "Próxima página", "ariaLabels.pageLabel": "Página {pageNumber} de todas as páginas", - "ariaLabels.previousPageLabel": "Página anterior" + "ariaLabels.previousPageLabel": "Página anterior", + "ariaLabels.jumpToPageButtonLabel": "Ir para a página", + "i18nStrings.jumpToPageInputLabel": "Página", + "i18nStrings.jumpToPageError": "Página fora do alcance. Mostrando a última página disponível." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Alça de redimensionamento do painel", diff --git a/src/i18n/messages/all.tr.json b/src/i18n/messages/all.tr.json index 7fb8a438bc..8d7cf0dc8b 100644 --- a/src/i18n/messages/all.tr.json +++ b/src/i18n/messages/all.tr.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "Sonraki sayfa", "ariaLabels.pageLabel": "Sayfa {pageNumber}/tüm sayfalar", - "ariaLabels.previousPageLabel": "Önceki sayfa" + "ariaLabels.previousPageLabel": "Önceki sayfa", + "ariaLabels.jumpToPageButtonLabel": "Sayfaya atla", + "i18nStrings.jumpToPageInputLabel": "Sayfa", + "i18nStrings.jumpToPageError": "Aralık dışı sayfa. Mevcut son sayfa gösteriliyor." }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "Panel yeniden boyutlandırma tutamacı", diff --git a/src/i18n/messages/all.zh-CN.json b/src/i18n/messages/all.zh-CN.json index a1a50aec6a..13c8b83a19 100644 --- a/src/i18n/messages/all.zh-CN.json +++ b/src/i18n/messages/all.zh-CN.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "下一页", "ariaLabels.pageLabel": "所有页面中的第 {pageNumber} 页", - "ariaLabels.previousPageLabel": "上一页" + "ariaLabels.previousPageLabel": "上一页", + "ariaLabels.jumpToPageButtonLabel": "跳转到页面", + "i18nStrings.jumpToPageInputLabel": "页面", + "i18nStrings.jumpToPageError": "页面超出范围。显示最后一个可用页面。" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "面板大小调整手柄", diff --git a/src/i18n/messages/all.zh-TW.json b/src/i18n/messages/all.zh-TW.json index a3e229d170..9e8de5b797 100644 --- a/src/i18n/messages/all.zh-TW.json +++ b/src/i18n/messages/all.zh-TW.json @@ -240,7 +240,10 @@ "pagination": { "ariaLabels.nextPageLabel": "下一頁", "ariaLabels.pageLabel": "所有頁面中的第 {pageNumber} 頁", - "ariaLabels.previousPageLabel": "上一頁" + "ariaLabels.previousPageLabel": "上一頁", + "ariaLabels.jumpToPageButtonLabel": "跳到頁面", + "i18nStrings.jumpToPageInputLabel": "頁面", + "i18nStrings.jumpToPageError": "頁面超出範圍。顯示最後一個可用頁面。" }, "panel-resize-handle": { "i18nStrings.resizeHandleAriaLabel": "面板調整大小控點", From 63980064c95be45a36d0a562bce847a283ec013a Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Fri, 9 Jan 2026 16:20:27 -0800 Subject: [PATCH 12/14] feat: add pagination - jump to page, update i18n, snapshots --- src/i18n/messages-types.ts | 17 +++++++++++------ src/i18n/messages/all.en.json | 6 ++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 6964a13e85..28bb167d3e 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -196,6 +196,8 @@ export interface I18nFormatArgTypes { "i18nStrings.endMonthLabel": never; "i18nStrings.endDateLabel": never; "i18nStrings.endTimeLabel": never; + "i18nStrings.datePlaceholder": never; + "i18nStrings.timePlaceholder": never; "i18nStrings.dateTimeConstraintText": never; "i18nStrings.dateConstraintText": never; "i18nStrings.slashedDateTimeConstraintText": never; @@ -223,12 +225,15 @@ export interface I18nFormatArgTypes { "i18nStrings.loadingText": never; } "error-boundary": { - "i18nStrings.headerText"?: never; - "i18nStrings.descriptionText"?: { - hasFeedback: boolean; - Feedback: (chunks: React.ReactNode[]) => React.ReactNode; - }; - "i18nStrings.refreshActionText"?: never; + "i18nStrings.headerText": never; + "i18nStrings.descriptionText": { + "hasFeedback": string; + } + "i18nStrings.refreshActionText": never; + } + "features-notification-drawer": { + "i18nStrings.title": never; + "i18nStrings.viewAll": never; } "file-token-group": { "i18nStrings.limitShowFewer": never; diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 7e88102e91..9585251aa1 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -158,6 +158,8 @@ "i18nStrings.endMonthLabel": "End month", "i18nStrings.endDateLabel": "End date", "i18nStrings.endTimeLabel": "End time", + "i18nStrings.datePlaceholder": "YYYY-MM-DD", + "i18nStrings.timePlaceholder": "hh:mm:ss", "i18nStrings.dateTimeConstraintText": "For date, use YYYY/MM/DD. For time, use 24 hr format.", "i18nStrings.dateConstraintText": "For date, use YYYY/MM/DD.", "i18nStrings.slashedDateTimeConstraintText": "For date, use YYYY/MM/DD. For time, use 24 hr format.", @@ -180,6 +182,10 @@ "i18nStrings.descriptionText": "{hasFeedback, select, true {Refresh to try again. We are tracking this issue, but you can share more information here.} other {Refresh to try again.}}", "i18nStrings.refreshActionText": "Refresh page" }, + "features-notification-drawer": { + "i18nStrings.title": "Latest feature releases", + "i18nStrings.viewAll": "View all feature releases" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Show fewer", "i18nStrings.limitShowMore": "Show more", From 980f7603a0b4b4edfb9a5e040230c51ff1834d53 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Thu, 29 Jan 2026 15:55:00 -0800 Subject: [PATCH 13/14] feat: pagination - jump to page: increase code coverage, bugfixes from initial bug bash --- pages/pagination/permutations.page.tsx | 7 +++ pages/table/jump-to-page-open-end.page.tsx | 58 ++++++++++++++++++++ src/input/__tests__/internal.test.tsx | 15 +++++ src/pagination/__tests__/pagination.test.tsx | 57 +++++++++++++++++++ src/pagination/internal.tsx | 21 +++++-- src/pagination/styles.scss | 8 ++- 6 files changed, 159 insertions(+), 7 deletions(-) diff --git a/pages/pagination/permutations.page.tsx b/pages/pagination/permutations.page.tsx index 16bdb3ecdb..61d240cc8f 100644 --- a/pages/pagination/permutations.page.tsx +++ b/pages/pagination/permutations.page.tsx @@ -14,6 +14,12 @@ const paginationLabels: PaginationProps.Labels = { nextPageLabel: 'Next page', previousPageLabel: 'Previous page', pageLabel: pageNumber => `Page ${pageNumber} of all pages`, + jumpToPageButton: 'Go to page', +}; + +const paginationI18nStrings: PaginationProps.I18nStrings = { + jumpToPageLabel: 'Page', + jumpToPageError: 'Enter a valid page number', }; const permutations = createPermutations([ @@ -28,6 +34,7 @@ const permutations = createPermutations([ pagesCount: [15], openEnd: [true, false], ariaLabels: [paginationLabels], + i18nStrings: [paginationI18nStrings], jumpToPage: [undefined, { loading: false }, { loading: true }], }, ]); diff --git a/pages/table/jump-to-page-open-end.page.tsx b/pages/table/jump-to-page-open-end.page.tsx index 0798a10d55..29cee566ae 100644 --- a/pages/table/jump-to-page-open-end.page.tsx +++ b/pages/table/jump-to-page-open-end.page.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useRef, useState } from 'react'; +import { CollectionPreferences } from '~components'; import I18nProvider from '~components/i18n'; import messages from '~components/i18n/messages/all.en'; import Pagination, { PaginationProps } from '~components/pagination'; @@ -56,6 +57,63 @@ function JumpToPageOpenEndContent() { { header: 'DNS Name', cell: (item: Instance) => item.dnsName || '-' }, ]} items={currentItems} + preferences={ + + } pagination={ { expect(element).toHaveAttribute('aria-describedby', 'description'); expect(element).not.toHaveAttribute('aria-invalid'); }); + +test('renders inline label when __inlineLabelText is provided', () => { + renderInput(); + + const label = createWrapper().find('label')!; + expect(label.getElement()).toHaveTextContent('Page'); + expect(label.getElement()).toHaveAttribute('for', 'test-input'); +}); + +test('renders input without inline label wrapper when __inlineLabelText is not provided', () => { + renderInput(); + + expect(createWrapper().find('label')).toBeNull(); + expect(createWrapper().findByClassName(styles['inline-label-wrapper'])).toBeNull(); +}); diff --git a/src/pagination/__tests__/pagination.test.tsx b/src/pagination/__tests__/pagination.test.tsx index 5840494462..a764dfd309 100644 --- a/src/pagination/__tests__/pagination.test.tsx +++ b/src/pagination/__tests__/pagination.test.tsx @@ -505,5 +505,62 @@ describe('jump to page', () => { expect(onChange).not.toHaveBeenCalled(); }); + + test('should not submit on Enter when input is empty', () => { + const onChange = jest.fn(); + const { wrapper } = renderPagination( + + ); + + const input = wrapper.findJumpToPageInput()!.findNativeInput().getElement(); + wrapper.findJumpToPageInput()!.setInputValue(''); + fireEvent.keyDown(input, { keyCode: 13, key: 'Enter' }); + + expect(onChange).not.toHaveBeenCalled(); + }); + }); + + describe('open-end error handling', () => { + test('should not show error popover content while loading in open-end mode', () => { + const ref = React.createRef(); + const { wrapper, rerender } = renderPagination( + + ); + + ref.current?.setError(true); + rerender( + + ); + + // Popover wrapper exists but content should not be visible while loading + const popover = wrapper.findJumpToPagePopover(); + expect(popover).not.toBeNull(); + expect(popover!.findContent()).toBeNull(); + }); + + test('should show error popover after loading completes in open-end mode', () => { + const ref = React.createRef(); + const { wrapper, rerender } = renderPagination( + + ); + + ref.current?.setError(true); + rerender( + + ); + + expect(wrapper.findJumpToPagePopover()).not.toBeNull(); + expect(wrapper.findJumpToPagePopover()!.findContent()).not.toBeNull(); + }); + + test('should sync input value after loading completes', () => { + const { wrapper, rerender } = renderPagination( + + ); + + rerender(); + + expect(wrapper.findJumpToPageInput()!.findNativeInput().getElement()).toHaveValue(3); + }); }); }); diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index c3591ced87..d43f08621e 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import clsx from 'clsx'; import { @@ -34,7 +34,7 @@ const defaultAriaLabels: Required = { }; const defaultI18nStrings: Required = { - jumpToPageLabel: '', + jumpToPageLabel: 'Page', jumpToPageError: '', }; @@ -134,10 +134,13 @@ const InternalPagination = React.forwardRef( const { leftDots, leftIndex, rightIndex, rightDots } = getPaginationState(currentPageIndex, pagesCount, openEnd); const [jumpToPageValue, setJumpToPageValue] = useState(currentPageIndex?.toString()); const prevLoadingRef = React.useRef(jumpToPage?.loading); + const jumpToPageInputRef = useRef(null); const [popoverVisible, setPopoverVisible] = useState(false); const [hasError, setHasError] = useState(false); const i18n = useInternalI18n('pagination'); + const i18nTutorial = useInternalI18n('tutorial-panel'); + const loadingText = i18nTutorial('i18nStrings.loadingText', 'Loading'); // Expose setError function via ref React.useImperativeHandle(ref, () => ({ @@ -190,6 +193,7 @@ const InternalPagination = React.forwardRef( function handleJumpToPageClick(requestedPageIndex: number) { if (requestedPageIndex < 1) { handlePageClick(1); + jumpToPageInputRef.current?.focus(); return; } @@ -205,6 +209,7 @@ const InternalPagination = React.forwardRef( handlePageClick(pagesCount, true); } } + jumpToPageInputRef.current?.focus(); } // Auto-clear error when user types in the input @@ -242,7 +247,8 @@ const InternalPagination = React.forwardRef( iconName="arrow-right" variant="icon" loading={jumpToPage?.loading} - ariaLabel={jumpToPageButtonLabel ?? defaultAriaLabels.jumpToPageButton} + loadingText={loadingText} + ariaLabel={jumpToPage?.loading ? loadingText : (jumpToPageButtonLabel ?? defaultAriaLabels.jumpToPageButton)} onClick={() => handleJumpToPageClick(Number(jumpToPageValue))} disabled={!jumpToPageValue || Number(jumpToPageValue) === currentPageIndex} /> @@ -316,18 +322,21 @@ const InternalPagination = React.forwardRef( {jumpToPage && ( -
    +
  • setPopoverVisible(false)} onKeyDown={e => { if (e.detail.keyCode === 13 && jumpToPageValue && Number(jumpToPageValue) !== currentPageIndex) { handleJumpToPageClick(Number(jumpToPageValue)); @@ -351,7 +360,7 @@ const InternalPagination = React.forwardRef( jumpToPageButton )} -
    +
  • )} ); diff --git a/src/pagination/styles.scss b/src/pagination/styles.scss index 576159e3df..5fbda0980c 100644 --- a/src/pagination/styles.scss +++ b/src/pagination/styles.scss @@ -85,11 +85,17 @@ margin-inline-start: awsui.$space-xs; padding-inline-start: awsui.$space-xs; padding-inline-start: 15px; + list-style: none; &-input { - max-inline-size: 87px; + inline-size: 87px; margin-block-start: -0.6em; overflow: visible; + + // stylelint-disable-next-line selector-max-universal + > * > * { + inline-size: 100%; + } } } From 815a40f94347fd928d90451a275ed693f156f496 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Mon, 2 Feb 2026 15:37:48 -0800 Subject: [PATCH 14/14] fix: restore auto-generated i18n messages-types.ts from upstream --- src/i18n/messages-types.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 28bb167d3e..6964a13e85 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -196,8 +196,6 @@ export interface I18nFormatArgTypes { "i18nStrings.endMonthLabel": never; "i18nStrings.endDateLabel": never; "i18nStrings.endTimeLabel": never; - "i18nStrings.datePlaceholder": never; - "i18nStrings.timePlaceholder": never; "i18nStrings.dateTimeConstraintText": never; "i18nStrings.dateConstraintText": never; "i18nStrings.slashedDateTimeConstraintText": never; @@ -225,15 +223,12 @@ export interface I18nFormatArgTypes { "i18nStrings.loadingText": never; } "error-boundary": { - "i18nStrings.headerText": never; - "i18nStrings.descriptionText": { - "hasFeedback": string; - } - "i18nStrings.refreshActionText": never; - } - "features-notification-drawer": { - "i18nStrings.title": never; - "i18nStrings.viewAll": never; + "i18nStrings.headerText"?: never; + "i18nStrings.descriptionText"?: { + hasFeedback: boolean; + Feedback: (chunks: React.ReactNode[]) => React.ReactNode; + }; + "i18nStrings.refreshActionText"?: never; } "file-token-group": { "i18nStrings.limitShowFewer": never;