From 3e76baa89012b9e911ee80a72d173a443098ea0a Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Mon, 5 Jan 2026 15:18:05 -0800 Subject: [PATCH 01/35] 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 ff0f1a847c..234ffbc35f 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 @@ -482,6 +482,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 f4dfd75eb81966fc0d796c4c22f9ba6987d5644d Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Mon, 5 Jan 2026 15:35:29 -0800 Subject: [PATCH 02/35] 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); @@ -54,6 +56,8 @@ function InternalPopover( __onOpen, __internalRootRef, __closeAnalyticsAction, + visible: controlledVisible, + onVisibleChange, ...restProps }: InternalPopoverProps, @@ -66,7 +70,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)) { @@ -78,13 +95,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) => { @@ -94,21 +111,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 4d8e9968232523d9d56462936de1abbfc115ad69 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Mon, 5 Jan 2026 16:21:15 -0800 Subject: [PATCH 03/35] 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 ef0ce2c3c492ebf4c126ddc568e4a09db46913cd Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Tue, 6 Jan 2026 16:27:23 +0100 Subject: [PATCH 04/35] 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 c20961d9f3d18910c640a5c99122f24b108957b0 Mon Sep 17 00:00:00 2001 From: Amanuel Abiy Date: Tue, 6 Jan 2026 16:53:04 +0100 Subject: [PATCH 05/35] 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 adaf406727..1048658ff1 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,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", @@ -113,7 +113,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", @@ -131,7 +131,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", @@ -165,7 +165,7 @@ "stylelint --fix" ], "package-lock.json": [ - "prepare-package-lock" + "node ./scripts/unlock-package-lock.js" ] }, "size-limit": [ From 93a17b645f6fa4fe1b92cd63207b66a918c231f2 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/35] 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 7116a18609..b15435bb5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10340,6 +10340,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 8c19d4a58cdfb09cc0f50156c6549c09b2fa07c8 Mon Sep 17 00:00:00 2001 From: Amanuel Abiy Date: Wed, 7 Jan 2026 16:54:01 +0100 Subject: [PATCH 07/35] 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 1048658ff1..b7b13e3a29 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,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 2271b678029c4e65594a06ff2844a928e1c4de74 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Wed, 7 Jan 2026 17:35:01 +0100 Subject: [PATCH 08/35] 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 44bed51eee3eccdceadb9156b8bef6e25c87aefe Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Fri, 9 Jan 2026 16:20:27 -0800 Subject: [PATCH 09/35] 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/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 +- 14 files changed, 575 insertions(+), 438 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 5fec0747f7..ece76561e6 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -18915,7 +18915,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": [ { @@ -18939,6 +18951,11 @@ Example: "inlineType": { "name": "PaginationProps.Labels", "properties": [ + { + "name": "jumpToPageButton", + "optional": true, + "type": "string", + }, { "name": "nextPageLabel", "optional": true, @@ -18990,6 +19007,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 @@ -23030,6 +23085,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, @@ -35648,6 +35708,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": [], @@ -35721,6 +35808,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": [ { @@ -40156,88 +40325,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": [ { @@ -46474,6 +46561,33 @@ To find a specific item use the \`findBreadcrumbLink(n)\` function as chaining \ "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": [], @@ -46524,6 +46638,79 @@ To find a specific item use the \`findBreadcrumbLink(n)\` function as chaining \ ], "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": [ { @@ -49645,79 +49832,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/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 25c6e84e46..1457e8b180 100644 --- a/src/popover/internal.tsx +++ b/src/popover/internal.tsx @@ -141,7 +141,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); } }; @@ -150,7 +150,7 @@ function InternalPopover( return () => { document.removeEventListener('mousedown', onDocumentClick); }; - }, []); + }, [updateVisible]); const popoverClasses = usePortalModeClasses(triggerRef, { resetVisualContext: true }); const { isInlineToken } = useTokenInlineContext(); 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 20119bf49dd44dba3b73f6ace28596334b0b6dcc Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Thu, 15 Jan 2026 15:42:41 -0800 Subject: [PATCH 10/35] Remove unrelated i18n changes --- src/i18n/messages/all.en.json | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 36a1352ef6..5c4b556f90 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -162,36 +162,6 @@ "i18nStrings.isoDatePlaceholder": "YYYY-MM-DD", "i18nStrings.slashedDatePlaceholder": "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.", - "i18nStrings.isoDateTimeConstraintText": "For date, use YYYY-MM-DD. For time, use 24 hr format.", - "i18nStrings.slashedDateConstraintText": "For date, use YYYY/MM/DD.", - "i18nStrings.isoDateConstraintText": "For date, use YYYY-MM-DD.", - "i18nStrings.slashedMonthConstraintText": "For month, use YYYY/MM.", - "i18nStrings.isoMonthConstraintText": "For month, use YYYY-MM.", - "i18nStrings.monthConstraintText": "For month, use YYYY/MM.", - "i18nStrings.errorIconAriaLabel": "Error", - "i18nStrings.renderSelectedAbsoluteRangeAriaLive": "Range selected from {startDate} to {endDate}", - "i18nStrings.formatRelativeRange": "{unit, select, second {{amount, plural, zero {Last {amount} seconds} one {Last {amount} second} other {Last {amount} seconds}}} minute {{amount, plural, zero {Last {amount} minutes} one {Last {amount} minute} other {Last {amount} minutes}}} hour {{amount, plural, zero {Last {amount} hours} one {Last {amount} hour} other {Last {amount} hours}}} day {{amount, plural, zero {Last {amount} days} one {Last {amount} day} other {Last {amount} days}}} week {{amount, plural, zero {Last {amount} weeks} one {Last {amount} week} other {Last {amount} weeks}}} month {{amount, plural, zero {Last {amount} months} one {Last {amount} month} other {Last {amount} months}}} year {{amount, plural, zero {Last {amount} years} one {Last {amount} year} other {Last {amount} years}}} other {}}", - "i18nStrings.formatUnit": "{unit, select, second {{amount, plural, zero {seconds} one {second} other {seconds}}} minute {{amount, plural, zero {minutes} one {minute} other {minutes}}} hour {{amount, plural, zero {hours} one {hour} other {hours}}} day {{amount, plural, zero {days} one {day} other {days}}} week {{amount, plural, zero {weeks} one {week} other {weeks}}} month {{amount, plural, zero {months} one {month} other {months}}} year {{amount, plural, zero {years} one {year} other {years}}} other {}}" - }, - "drawer": { - "i18nStrings.loadingText": "Loading content" - }, - "error-boundary": { - "i18nStrings.headerText": "Unexpected error, content failed to show", - "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", - "ariaLabels.closeButton": "Close notifications", - "ariaLabels.content": "Feature notifications", - "ariaLabels.triggerButton": "Show feature notifications", - "ariaLabels.resizeHandle": "Resize feature notifications" - }, "file-token-group": { "i18nStrings.limitShowFewer": "Show fewer", "i18nStrings.limitShowMore": "Show more", @@ -485,3 +455,4 @@ } } + From f2bd22e98fc946f9ba4a61a1603c2890c0f11289 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Thu, 29 Jan 2026 15:55:00 -0800 Subject: [PATCH 11/35] 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 4a2980ca604769965296872ff2111278a36c37ab Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Thu, 5 Feb 2026 12:26:02 -0800 Subject: [PATCH 12/35] fix: mark visible/onVisibleChange as internal-only props and remove unrelated changes --- package-lock.json | 16 ---------------- package.json | 6 +++--- src/pagination/internal.tsx | 4 ++-- src/popover/internal.tsx | 8 ++++---- 4 files changed, 9 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index b15435bb5e..7116a18609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10340,22 +10340,6 @@ "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, diff --git a/package.json b/package.json index b7b13e3a29..adaf406727 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.2", "loader-utils": "^3.2.1", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "mini-css-extract-plugin": "^2.4.4", "mississippi": "^4.0.0", "mockdate": "^3.0.5", @@ -131,7 +131,7 @@ "rollup-plugin-license": "^3.0.1", "sass": "^1.89.2", "sass-loader": "^12.3.0", - "size-limit": "^11.1.6", + "size-limit": "^12.0.0", "stylelint": "^16.6.1", "stylelint-config-recommended-scss": "^14.0.0", "stylelint-no-unsupported-browser-features": "^8.0.2", @@ -165,7 +165,7 @@ "stylelint --fix" ], "package-lock.json": [ - "node ./scripts/unlock-package-lock.js" + "prepare-package-lock" ] }, "size-limit": [ diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index d43f08621e..b376c43d30 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -348,11 +348,11 @@ const InternalPagination = React.forwardRef( setPopoverVisible(detail.visible)} + __onVisibleChange={({ detail }) => setPopoverVisible(detail.visible)} > {jumpToPageButton} diff --git a/src/popover/internal.tsx b/src/popover/internal.tsx index 1457e8b180..03793d6631 100644 --- a/src/popover/internal.tsx +++ b/src/popover/internal.tsx @@ -30,8 +30,8 @@ export interface InternalPopoverProps extends Omit; + __visible?: boolean; + __onVisibleChange?: NonCancelableEventHandler<{ visible: boolean }>; } export default React.forwardRef(InternalPopover); @@ -56,8 +56,8 @@ function InternalPopover( __onOpen, __internalRootRef, __closeAnalyticsAction, - visible: controlledVisible, - onVisibleChange, + __visible: controlledVisible, + __onVisibleChange: onVisibleChange, ...restProps }: InternalPopoverProps, From d44382581af45845d9ddb5ce890209d0e5f561e5 Mon Sep 17 00:00:00 2001 From: Joan Perals Date: Fri, 6 Feb 2026 12:51:36 +0100 Subject: [PATCH 13/35] chore: Refactor card into separate internal component (#4084) --- pages/card/common.tsx | 67 ++++++++++++ pages/card/header-permutations.page.tsx | 45 +++++++++ pages/card/permutations.page.tsx | 30 ++++++ src/cards/index.tsx | 1 + src/internal/components/card/index.tsx | 61 +++++++++++ src/internal/components/card/interfaces.ts | 57 +++++++++++ src/internal/components/card/styles.scss | 112 +++++++++++++++++++++ src/item-card/motion.scss | 4 +- 8 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 pages/card/common.tsx create mode 100644 pages/card/header-permutations.page.tsx create mode 100644 pages/card/permutations.page.tsx create mode 100644 src/internal/components/card/index.tsx create mode 100644 src/internal/components/card/interfaces.ts create mode 100644 src/internal/components/card/styles.scss diff --git a/pages/card/common.tsx b/pages/card/common.tsx new file mode 100644 index 0000000000..a90ad2b91d --- /dev/null +++ b/pages/card/common.tsx @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ReactNode, useContext } from 'react'; +import React from 'react'; + +import ButtonGroup from '~components/button-group'; +import FormField from '~components/form-field'; +import Icon from '~components/icon'; +import Input from '~components/input'; +import SpaceBetween from '~components/space-between'; + +import AppContext, { AppContextType } from '../app/app-context'; +import ScreenshotArea from '../utils/screenshot-area'; + +type PageContext = React.Context< + AppContextType<{ + containerWidth?: string; + }> +>; + +export function CardPage({ title, children }: { title: string; children: ReactNode; settings?: ReactNode }) { + const { urlParams, setUrlParams } = useContext(AppContext as PageContext); + const containerWidth = urlParams.containerWidth || '400'; + return ( +
    +

    {title}

    + +
    + + setUrlParams({ containerWidth: detail.value })} + type="number" + inputMode="numeric" + /> + +
    +
    +
    +
    + {children} +
    +
    + ); +} + +export const shortHeader = 'Card Header'; +export const longHeader = 'This is a very long card header that might wrap to multiple lines on smaller viewports'; + +export const shortDescription = 'Short description'; +export const longDescription = + 'This is a long description that provides more context and details about the card content.'; + +export const longContent = + 'This is long card content with multiple sentences. It provides more detailed information and might wrap across several lines.'; + +export const actions = ( + +); + +export const icon = ; diff --git a/pages/card/header-permutations.page.tsx b/pages/card/header-permutations.page.tsx new file mode 100644 index 0000000000..4fa20d613e --- /dev/null +++ b/pages/card/header-permutations.page.tsx @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import Card from '~components/internal/components/card'; +import { InternalCardProps } from '~components/internal/components/card/interfaces'; + +import createPermutations from '../utils/permutations'; +import PermutationsView from '../utils/permutations-view'; +import { actions, CardPage, icon, longContent, longDescription, longHeader, shortHeader } from './common'; + +const permutations = createPermutations([ + // With header + { + header: [shortHeader, longHeader], + children: [longContent], + description: [undefined, longDescription], + actions: [undefined, actions], + icon: [undefined, icon], + }, + // Without header and without description + { + header: [undefined], + children: [longContent], + description: [undefined], + actions: [undefined, actions], + icon: [undefined, icon], + }, + // Without header but with description + { + header: [undefined], + children: [longContent], + description: [longDescription], + actions: [undefined, actions], + icon: [undefined], + }, +]); + +export default function CardHeaderPermutations() { + return ( + + } /> + + ); +} diff --git a/pages/card/permutations.page.tsx b/pages/card/permutations.page.tsx new file mode 100644 index 0000000000..651445755d --- /dev/null +++ b/pages/card/permutations.page.tsx @@ -0,0 +1,30 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import Card from '~components/internal/components/card'; +import { InternalCardProps } from '~components/internal/components/card/interfaces'; + +import createPermutations from '../utils/permutations'; +import PermutationsView from '../utils/permutations-view'; +import { actions, CardPage, icon, longContent, longHeader, shortDescription } from './common'; + +const permutations = createPermutations([ + { + header: [longHeader, undefined], + children: [longContent, undefined], + description: [shortDescription, undefined], + actions: [actions], + icon: [icon], + disableHeaderPaddings: [false, true], + disableContentPaddings: [false, true], + }, +]); + +export default function CardPermutations() { + return ( + + } /> + + ); +} diff --git a/src/cards/index.tsx b/src/cards/index.tsx index b88d98a922..587463dbbf 100644 --- a/src/cards/index.tsx +++ b/src/cards/index.tsx @@ -12,6 +12,7 @@ import { InternalContainerAsSubstep } from '../container/internal'; import { useInternalI18n } from '../i18n/context'; import { AnalyticsFunnelSubStep } from '../internal/analytics/components/analytics-funnel'; import { getBaseProps } from '../internal/base-component'; +import Card from '../internal/components/card'; import { CollectionLabelContext } from '../internal/context/collection-label-context'; import { LinkDefaultVariantContext } from '../internal/context/link-default-variant-context'; import useBaseComponent from '../internal/hooks/use-base-component'; diff --git a/src/internal/components/card/index.tsx b/src/internal/components/card/index.tsx new file mode 100644 index 0000000000..3cecc4ddc6 --- /dev/null +++ b/src/internal/components/card/index.tsx @@ -0,0 +1,61 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import clsx from 'clsx'; + +import { useVisualRefresh } from '../../hooks/use-visual-mode'; +import InternalStructuredItem from '../structured-item'; +import { InternalCardProps } from './interfaces'; + +import styles from './styles.css.js'; + +export default function Card({ + actions, + selected, + children, + className, + header, + description, + icon, + metadataAttributes, + onClick, + disableHeaderPaddings, + disableContentPaddings, +}: InternalCardProps) { + const isRefresh = useVisualRefresh(); + + const headerRowEmpty = !header && !description && !icon && !actions; + + return ( +
    +
    + {header}
    } + secondaryContent={description &&
    {description}
    } + icon={icon} + actions={actions} + disablePaddings={disableHeaderPaddings} + wrapActions={false} + /> +
    +
    {children}
    +
    + ); +} diff --git a/src/internal/components/card/interfaces.ts b/src/internal/components/card/interfaces.ts new file mode 100644 index 0000000000..adf72487b9 --- /dev/null +++ b/src/internal/components/card/interfaces.ts @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import { BaseComponentProps } from '../../base-component'; + +export interface BaseCardProps extends BaseComponentProps { + /** + * Heading text. + */ + header?: React.ReactNode; + + /** + * Supplementary text below the heading. + */ + description?: React.ReactNode; + + /** + * Specifies actions for the card. + */ + actions?: React.ReactNode; + + /** + * Primary content displayed in the card. + */ + children?: React.ReactNode; + + /** + * Icon which will be displayed at the top of the card, + * aligned with the start of the content. + */ + icon?: React.ReactNode; + + /** + * Determines whether the card header has padding. If `true`, removes the default padding from the header. + */ + disableHeaderPaddings?: boolean; + + /** + * Determines whether the card content has padding. If `true`, removes the default padding from the content area. + */ + disableContentPaddings?: boolean; +} + +export interface InternalCardProps extends BaseCardProps { + /** + * Called when the user clicks on the card. + */ + onClick?: React.MouseEventHandler; + + /** + * Specifies whether the card is in active state. + */ + selected?: boolean; + + metadataAttributes?: Record; +} diff --git a/src/internal/components/card/styles.scss b/src/internal/components/card/styles.scss new file mode 100644 index 0000000000..1c621d4af7 --- /dev/null +++ b/src/internal/components/card/styles.scss @@ -0,0 +1,112 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use 'sass:math'; + +@use '../../styles' as styles; +@use '../../styles/tokens' as awsui; +@use './motion'; + +@mixin card-style { + border-start-start-radius: awsui.$border-radius-container; + border-start-end-radius: awsui.$border-radius-container; + border-end-start-radius: awsui.$border-radius-container; + border-end-end-radius: awsui.$border-radius-container; + box-sizing: border-box; + + &::before { + @include styles.base-pseudo-element; + // Reset border color to prevent it from flashing black during card selection animation + border-color: transparent; + border-block-start: awsui.$border-container-top-width solid awsui.$color-border-container-top; + border-start-start-radius: awsui.$border-radius-container; + border-start-end-radius: awsui.$border-radius-container; + border-end-start-radius: awsui.$border-radius-container; + border-end-end-radius: awsui.$border-radius-container; + z-index: 1; + } + + &::after { + @include styles.base-pseudo-element; + border-start-start-radius: awsui.$border-radius-container; + border-start-end-radius: awsui.$border-radius-container; + border-end-start-radius: awsui.$border-radius-container; + border-end-end-radius: awsui.$border-radius-container; + } + &:not(.refresh)::after { + box-shadow: awsui.$shadow-container; + } + &.refresh::after { + border-block: solid awsui.$border-divider-section-width awsui.$color-border-divider-default; + border-inline: solid awsui.$border-divider-section-width awsui.$color-border-divider-default; + } +} + +.root { + @include styles.styles-reset(); + box-sizing: border-box; + position: relative; + background-color: awsui.$color-background-container-content; + min-inline-size: 0; + @include card-style; +} + +$button-padding-vertical: styles.$control-padding-vertical; +$button-padding-horizontal: awsui.$space-xxs; +$padding-block-start-base: calc(awsui.$space-card-vertical - $button-padding-vertical); + +.header { + &:not(.no-padding) { + padding-block-end: awsui.$space-xxs; + padding-inline-start: awsui.$space-card-horizontal; + &:not(.with-actions) { + padding-block-start: $padding-block-start-base; + padding-inline-end: awsui.$space-card-horizontal; + } + &.with-actions { + // Compensate the padding given by the buttons in the actions slot + padding-block-start: calc($padding-block-start-base - $button-padding-vertical); + padding-inline-end: calc(awsui.$space-card-horizontal - $button-padding-horizontal); + } + } + &-inner { + @include styles.font-heading-s; + } +} + +.body { + &:not(.no-padding) { + padding-block-start: awsui.$space-xxs; + padding-block-end: awsui.$space-card-vertical; + padding-inline: awsui.$space-card-horizontal; + } + &.no-padding { + border-end-start-radius: awsui.$border-radius-container; + border-end-end-radius: awsui.$border-radius-container; + } +} + +.no-header, +.no-content { + // No need to preserve the space between header and content when one of them is missing. + > .header:not(.no-padding) { + padding-block-end: 0; + } + > .body:not(.no-padding) { + padding-block-start: 0; + } +} + +.selected { + background-color: awsui.$color-background-item-selected; + &::before { + border-block: awsui.$border-item-width solid awsui.$color-border-item-selected; + border-inline: awsui.$border-item-width solid awsui.$color-border-item-selected; + } +} + +.description { + color: awsui.$color-text-heading-secondary; +} diff --git a/src/item-card/motion.scss b/src/item-card/motion.scss index 1cf811db27..d17e521a8b 100644 --- a/src/item-card/motion.scss +++ b/src/item-card/motion.scss @@ -3,8 +3,8 @@ SPDX-License-Identifier: Apache-2.0 */ -@use '../internal/styles' as styles; -@use '../internal/styles/tokens' as awsui; +@use '../../styles' as styles; +@use '../../styles/tokens' as awsui; .root { @include styles.with-motion { From 0593dc6b485f1d3fdece10e2a269160b7c9ef1af Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Mon, 9 Feb 2026 09:38:36 +0100 Subject: [PATCH 14/35] chore: Implements grouped data support in table (WIP) (#3673) --- src/table/expandable-rows/expandable-rows-utils.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/table/expandable-rows/expandable-rows-utils.ts b/src/table/expandable-rows/expandable-rows-utils.ts index 476cdcd626..6514eb1fb7 100644 --- a/src/table/expandable-rows/expandable-rows-utils.ts +++ b/src/table/expandable-rows/expandable-rows-utils.ts @@ -64,14 +64,14 @@ export function useExpandableTableProps({ if (visible) { visibleItems.push(item); - children.forEach((child, index) => - traverse( - child, - { level: detail.level + 1, setSize: children.length, posInSet: index + 1, parent: item }, - expandedSet.has(item) - ) - ); } + children.forEach((child, index) => + traverse( + child, + { level: detail.level + 1, setSize: children.length, posInSet: index + 1, parent: item }, + visible && expandedSet.has(item) + ) + ); }; items.forEach((item, index) => traverse(item, { level: 1, setSize: items.length, posInSet: index + 1, parent: null }, true) From e268562f3655a7fada53c0a0f055ea262bc2f991 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Mon, 9 Feb 2026 19:09:51 +0100 Subject: [PATCH 15/35] fix: Fixes tables with infinite expandable rows (#4235) --- src/table/expandable-rows/expandable-rows-utils.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/table/expandable-rows/expandable-rows-utils.ts b/src/table/expandable-rows/expandable-rows-utils.ts index 6514eb1fb7..476cdcd626 100644 --- a/src/table/expandable-rows/expandable-rows-utils.ts +++ b/src/table/expandable-rows/expandable-rows-utils.ts @@ -64,14 +64,14 @@ export function useExpandableTableProps({ if (visible) { visibleItems.push(item); + children.forEach((child, index) => + traverse( + child, + { level: detail.level + 1, setSize: children.length, posInSet: index + 1, parent: item }, + expandedSet.has(item) + ) + ); } - children.forEach((child, index) => - traverse( - child, - { level: detail.level + 1, setSize: children.length, posInSet: index + 1, parent: item }, - visible && expandedSet.has(item) - ) - ); }; items.forEach((item, index) => traverse(item, { level: 1, setSize: items.length, posInSet: index + 1, parent: null }, true) From 20d993173dacdd156bcc475b93a87f3f87c4aa7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:53:35 +0100 Subject: [PATCH 16/35] chore: Bump webpack from 5.99.9 to 5.105.0 (#4226) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Georgii Lobko --- package-lock.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7116a18609..941ec0a2d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15273,9 +15273,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } From 0ec496e0ba7f11259bcd7dd4d1a470fc97b707af Mon Sep 17 00:00:00 2001 From: Philipp Schneider <115801132+Who-is-PS@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:05:42 +0100 Subject: [PATCH 17/35] feat: Add datePlaceholder and timePlaceholder props to date-range-picker component's i18nStrings (#4104) --- src/i18n/messages/all.en.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 5c4b556f90..79449e9d39 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -162,6 +162,28 @@ "i18nStrings.isoDatePlaceholder": "YYYY-MM-DD", "i18nStrings.slashedDatePlaceholder": "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.", + "i18nStrings.isoDateTimeConstraintText": "For date, use YYYY-MM-DD. For time, use 24 hr format.", + "i18nStrings.slashedDateConstraintText": "For date, use YYYY/MM/DD.", + "i18nStrings.isoDateConstraintText": "For date, use YYYY-MM-DD.", + "i18nStrings.slashedMonthConstraintText": "For month, use YYYY/MM.", + "i18nStrings.isoMonthConstraintText": "For month, use YYYY-MM.", + "i18nStrings.monthConstraintText": "For month, use YYYY/MM.", + "i18nStrings.errorIconAriaLabel": "Error", + "i18nStrings.renderSelectedAbsoluteRangeAriaLive": "Range selected from {startDate} to {endDate}", + "i18nStrings.formatRelativeRange": "{unit, select, second {{amount, plural, zero {Last {amount} seconds} one {Last {amount} second} other {Last {amount} seconds}}} minute {{amount, plural, zero {Last {amount} minutes} one {Last {amount} minute} other {Last {amount} minutes}}} hour {{amount, plural, zero {Last {amount} hours} one {Last {amount} hour} other {Last {amount} hours}}} day {{amount, plural, zero {Last {amount} days} one {Last {amount} day} other {Last {amount} days}}} week {{amount, plural, zero {Last {amount} weeks} one {Last {amount} week} other {Last {amount} weeks}}} month {{amount, plural, zero {Last {amount} months} one {Last {amount} month} other {Last {amount} months}}} year {{amount, plural, zero {Last {amount} years} one {Last {amount} year} other {Last {amount} years}}} other {}}", + "i18nStrings.formatUnit": "{unit, select, second {{amount, plural, zero {seconds} one {second} other {seconds}}} minute {{amount, plural, zero {minutes} one {minute} other {minutes}}} hour {{amount, plural, zero {hours} one {hour} other {hours}}} day {{amount, plural, zero {days} one {day} other {days}}} week {{amount, plural, zero {weeks} one {week} other {weeks}}} month {{amount, plural, zero {months} one {month} other {months}}} year {{amount, plural, zero {years} one {year} other {years}}} other {}}" + }, + "drawer": { + "i18nStrings.loadingText": "Loading content" + }, + "error-boundary": { + "i18nStrings.headerText": "Unexpected error, content failed to show", + "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" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Show fewer", "i18nStrings.limitShowMore": "Show more", From c4dfd442997eea9ad84e637d23aaa152e4984341 Mon Sep 17 00:00:00 2001 From: Philipp Schneider <115801132+Who-is-PS@users.noreply.github.com> Date: Fri, 13 Feb 2026 13:44:56 +0100 Subject: [PATCH 18/35] feat: Add disabled filter option to test utils to button-dropdown (#4244) --- src/test-utils/dom/button-dropdown/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test-utils/dom/button-dropdown/index.ts b/src/test-utils/dom/button-dropdown/index.ts index ef7b1665fb..c7ec381473 100644 --- a/src/test-utils/dom/button-dropdown/index.ts +++ b/src/test-utils/dom/button-dropdown/index.ts @@ -24,6 +24,18 @@ function getItemSelector({ disabled }: { disabled?: boolean }): string { return selector; } +function getItemSelector({ disabled }: { disabled?: boolean }): string { + let selector = `.${itemStyles['item-element']}`; + + if (disabled === true) { + selector += `.${itemStyles.disabled}`; + } else if (disabled === false) { + selector += `:not(.${itemStyles.disabled})`; + } + + return selector; +} + export default class ButtonDropdownWrapper extends ComponentWrapper { static rootSelector: string = styles['button-dropdown']; From 918b304d6779a4a4c06de59adbbf6631007a9791 Mon Sep 17 00:00:00 2001 From: srungta08 Date: Fri, 13 Feb 2026 13:52:10 +0100 Subject: [PATCH 19/35] test: add classic theme tests for drawer filtering test (#4245) --- .../__tests__/runtime-drawers.test.tsx | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index 6204789c58..3bfda7c950 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -1135,6 +1135,10 @@ describe('toolbar mode only features', () => { mountContent: container => (container.textContent = 'global drawer content 1'), }); + if (await validateThemeCompatibility(theme, )) { + return; + } + const renderProps = await renderComponent(); const { globalDrawersWrapper } = renderProps; @@ -1162,6 +1166,9 @@ describe('toolbar mode only features', () => { preserveInactiveContent: true, }); + if (await validateThemeCompatibility(theme, )) { + return; + } const renderProps = await renderComponent(); const { globalDrawersWrapper } = renderProps; @@ -1198,6 +1205,10 @@ describe('toolbar mode only features', () => { preserveInactiveContent: true, }); + if (await validateThemeCompatibility(theme, )) { + return; + } + const renderProps = await renderComponent(); const { globalDrawersWrapper } = renderProps; @@ -1221,6 +1232,10 @@ describe('toolbar mode only features', () => { test(`closes a drawer when closeDrawer is called (${type} drawer)`, async () => { registerDrawer({ ...drawerDefaults, resizable: true }); + if (await validateThemeCompatibility(theme, )) { + return; + } + const { wrapper } = await renderComponent(); await delay(); @@ -1251,6 +1266,10 @@ describe('toolbar mode only features', () => { }); test('should render trigger buttons for global drawers even if local drawers are not present', async () => { + if (await validateThemeCompatibility(theme, )) { + return; + } + const renderProps = await renderComponent(); registerDrawer({ @@ -1275,6 +1294,11 @@ describe('toolbar mode only features', () => { onToggle: event => onToggle(event.detail), }); +======= + if (await validateThemeCompatibility(theme, )) { + return; + } +>>>>>>> 7bdbb71cb (test: add classic theme tests for drawer filtering test (#4245)) const renderProps = await renderComponent(); await delay(); @@ -1371,6 +1395,10 @@ describe('toolbar mode only features', () => { mountContent: container => (container.textContent = 'global drawer content 2'), }); + if (await validateThemeCompatibility(theme, )) { + return; + } + const { wrapper, globalDrawersWrapper } = await renderComponent(); await delay(); @@ -1394,6 +1422,10 @@ describe('toolbar mode only features', () => { mountContent: container => (container.textContent = 'global drawer content 1'), }); + if (await validateThemeCompatibility(theme, )) { + return; + } + const { wrapper, globalDrawersWrapper } = await renderComponent(); await delay(); @@ -1420,6 +1452,24 @@ describe('toolbar mode only features', () => { trigger: undefined, }); + if ( + await validateThemeCompatibility( + theme, + + + + } + /> + ) + ) { + return; + } + const { globalDrawersWrapper, getByTestId } = await renderComponent( { test('should not render a trigger button if registered drawer does not have a trigger prop', async () => { awsuiPlugins.appLayout.registerDrawer({ ...drawerDefaults, trigger: undefined }); + if (await validateThemeCompatibility(theme, )) { + return; + } + const { wrapper } = await renderComponent(); await delay(); @@ -1506,6 +1560,10 @@ describe('toolbar mode only features', () => { describe('dynamically registered drawers with defaultActive: true', () => { test('should open if there are already open local drawer on the page', async () => { + if (await validateThemeCompatibility(theme, )) { + return; + } + const { wrapper, globalDrawersWrapper } = await renderComponent(); wrapper.findDrawerTriggerById('security')!.click(); @@ -1541,6 +1599,10 @@ describe('toolbar mode only features', () => { }); test('should not open if there are already global drawers opened by user action on the page', async () => { + if (await validateThemeCompatibility(theme, )) { + return; + } + const { wrapper, globalDrawersWrapper } = await renderComponent(); wrapper.findDrawerTriggerById('security')!.click(); @@ -1576,6 +1638,10 @@ describe('toolbar mode only features', () => { }); test('should not open if the maximum number (2) of global drawers is already open on the page', async () => { + if (await validateThemeCompatibility(theme, )) { + return; + } + const { wrapper, globalDrawersWrapper } = await renderComponent(); wrapper.findDrawerTriggerById('security')!.click(); @@ -1622,6 +1688,10 @@ describe('toolbar mode only features', () => { }); test('should open global bottom drawer by default when defaultActive is set', async () => { + if (await validateThemeCompatibility(theme, )) { + return; + } + const { globalDrawersWrapper } = await renderComponent(); awsuiWidgetPlugins.registerBottomDrawer({ @@ -2059,6 +2129,10 @@ describe('toolbar mode only features', () => { awsuiWidgetPlugins.registerLeftDrawer({ ...drawerDefaults, id: 'test2' }); awsuiWidgetPlugins.registerBottomDrawer({ ...drawerDefaults, id: 'bottom' }); + if (await validateThemeCompatibility(theme, )) { + return; + } + const { globalDrawersWrapper } = await renderComponent(); await delay(); @@ -2115,6 +2189,9 @@ describe('toolbar mode only features', () => { }); test('calls onResize handler for bottom drawer', async () => { + if (await validateThemeCompatibility(theme, )) { + return; + } const { wrapper, globalDrawersWrapper, debug } = await renderComponent(); const onResize = jest.fn(); awsuiWidgetPlugins.registerBottomDrawer({ @@ -2197,6 +2274,10 @@ describe('toolbar mode only features', () => { test('assigns correct ARIA roles when mixing global and regular drawers', async () => { registerGlobalDrawers(2); + + if (await validateThemeCompatibility(theme, )) { + return; + } const { wrapper } = await renderComponent(); await delay(); @@ -2225,6 +2306,10 @@ describe('toolbar mode only features', () => { test('assigns menuitemcheckbox role to global drawers in overflow menu', async () => { registerGlobalDrawers(3); + if (await validateThemeCompatibility(theme, )) { + return; + } + // In mobile view, two drawers are visible in the toolbar, the others are placed in the overflow menu const { wrapper, globalDrawersWrapper } = await renderComponent(); @@ -2259,6 +2344,9 @@ describe('toolbar mode only features', () => { type: 'global', isExpandable: true, }); + if (await validateThemeCompatibility(theme, )) { + return; + } const { wrapper, globalDrawersWrapper } = await renderComponent(); await delay(); From 1ed9d2612ffc51180ebdc1cc36b445f8f18f9659 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Fri, 9 Jan 2026 16:20:27 -0800 Subject: [PATCH 20/35] feat: add pagination - jump to page, update i18n, snapshots --- src/i18n/messages/all.en.json | 4 ++++ src/pagination/internal.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 79449e9d39..a2d4c434d2 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -184,6 +184,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/pagination/internal.tsx b/src/pagination/internal.tsx index b376c43d30..a0d4a18c3c 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -34,7 +34,7 @@ const defaultAriaLabels: Required = { }; const defaultI18nStrings: Required = { - jumpToPageLabel: 'Page', + jumpToPageLabel: '', jumpToPageError: '', }; From 9912009f03f139eabd5f9714ac84202ae881cbf5 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Thu, 15 Jan 2026 15:42:41 -0800 Subject: [PATCH 21/35] Remove unrelated i18n changes --- src/i18n/messages/all.en.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index a2d4c434d2..79449e9d39 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -184,10 +184,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", From c9e8398d1d5ea35873cfbadb27d5365804f39015 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Fri, 9 Jan 2026 16:20:27 -0800 Subject: [PATCH 22/35] feat: add pagination - jump to page, update i18n, snapshots --- src/i18n/messages/all.en.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 79449e9d39..a2d4c434d2 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -184,6 +184,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 f29f859ef08265f41044782a036dd020689922c8 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Thu, 5 Feb 2026 12:26:02 -0800 Subject: [PATCH 23/35] fix: mark visible/onVisibleChange as internal-only props and remove unrelated changes --- src/i18n/messages/all.en.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index a2d4c434d2..79449e9d39 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -184,10 +184,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", From 5bf37b0642d3d8c342e2b647510b1331dd7ef716 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Mon, 16 Feb 2026 17:11:35 -0800 Subject: [PATCH 24/35] chore: add loading text for jump to page, remove default fallbacks for i18n, track popover visibility from error state --- pages/pagination/permutations.page.tsx | 2 +- src/i18n/messages/all.ar.json | 2 +- src/i18n/messages/all.de.json | 2 +- src/i18n/messages/all.en-GB.json | 12 ++--- src/i18n/messages/all.en.json | 8 ++++ src/i18n/messages/all.es.json | 2 +- src/i18n/messages/all.fr.json | 16 +++---- src/i18n/messages/all.id.json | 6 +-- src/i18n/messages/all.it.json | 6 +-- src/i18n/messages/all.ja.json | 2 +- src/i18n/messages/all.ko.json | 2 +- src/i18n/messages/all.pt-BR.json | 8 ++-- src/i18n/messages/all.th.json | 2 +- src/i18n/messages/all.tr.json | 2 +- src/i18n/messages/all.zh-CN.json | 2 +- src/i18n/messages/all.zh-TW.json | 2 +- src/input/internal.tsx | 13 ++++- src/input/styles.scss | 8 ++++ src/pagination/interfaces.ts | 9 +++- src/pagination/internal.tsx | 66 +++++++------------------- src/pagination/styles.scss | 5 -- 21 files changed, 87 insertions(+), 90 deletions(-) diff --git a/pages/pagination/permutations.page.tsx b/pages/pagination/permutations.page.tsx index 61d240cc8f..34c8afff9c 100644 --- a/pages/pagination/permutations.page.tsx +++ b/pages/pagination/permutations.page.tsx @@ -18,7 +18,7 @@ const paginationLabels: PaginationProps.Labels = { }; const paginationI18nStrings: PaginationProps.I18nStrings = { - jumpToPageLabel: 'Page', + jumpToPageInputLabel: 'Page', jumpToPageError: 'Enter a valid page number', }; diff --git a/src/i18n/messages/all.ar.json b/src/i18n/messages/all.ar.json index a5f8e80f0e..5b8d36d780 100644 --- a/src/i18n/messages/all.ar.json +++ b/src/i18n/messages/all.ar.json @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "تحميل الخطوة التالية", "i18nStrings.submitButtonLoadingAnnouncement": "إرسال النموذج" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.de.json b/src/i18n/messages/all.de.json index 67e1f87187..5f0a86f2e2 100644 --- a/src/i18n/messages/all.de.json +++ b/src/i18n/messages/all.de.json @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Nächster Schritt wird geladen", "i18nStrings.submitButtonLoadingAnnouncement": "Absenden des Formulars" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.en-GB.json b/src/i18n/messages/all.en-GB.json index f23c65ab3b..bc1653358b 100644 --- a/src/i18n/messages/all.en-GB.json +++ b/src/i18n/messages/all.en-GB.json @@ -159,17 +159,17 @@ "i18nStrings.endDateLabel": "End date", "i18nStrings.endTimeLabel": "End time", "i18nStrings.datePlaceholder": "DD/MM/YYYY", - "i18nStrings.isoDatePlaceholder": "YYYY-MM-DD", - "i18nStrings.slashedDatePlaceholder": "YYYY/MM/DD", + "i18nStrings.isoDatePlaceholder": "DD-MM-YYYY", + "i18nStrings.slashedDatePlaceholder": "DD/MM/YYYY", "i18nStrings.timePlaceholder": "hh:mm:ss", "i18nStrings.dateTimeConstraintText": "For date, use DD/MM/YYYY. For time, use 24-hr format.", "i18nStrings.dateConstraintText": "For date, use YYYY/MM/DD.", "i18nStrings.slashedDateTimeConstraintText": "For date, use DD/MM/YYYY. For time, use 24-hr format.", - "i18nStrings.isoDateTimeConstraintText": "For date, use DD-MM-YYYY. For time, use 24-hr format.", + "i18nStrings.isoDateTimeConstraintText": "For date, use DD/MM/YYYY. For time, use 24-hr format.", "i18nStrings.slashedDateConstraintText": "For date, use DD/MM/YYYY.", - "i18nStrings.isoDateConstraintText": "For date, use DD-MM-YYYY.", + "i18nStrings.isoDateConstraintText": "For date, use DD/MM/YYYY.", "i18nStrings.slashedMonthConstraintText": "For month, use MM/YYYY.", - "i18nStrings.isoMonthConstraintText": "For month, use MM-YYYY.", + "i18nStrings.isoMonthConstraintText": "For month, use MM/YYYY.", "i18nStrings.monthConstraintText": "For month, use YYYY/MM.", "i18nStrings.errorIconAriaLabel": "Error", "i18nStrings.renderSelectedAbsoluteRangeAriaLive": "Range selected from {startDate} to {endDate}", @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Loading next step", "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 79449e9d39..d9bb541301 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -184,6 +184,14 @@ "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", + "ariaLabels.closeButton": "Close notifications", + "ariaLabels.content": "Feature notifications", + "ariaLabels.triggerButton": "Show feature notifications", + "ariaLabels.resizeHandle": "Resize feature notifications" + }, "file-token-group": { "i18nStrings.limitShowFewer": "Show fewer", "i18nStrings.limitShowMore": "Show more", diff --git a/src/i18n/messages/all.es.json b/src/i18n/messages/all.es.json index 43c12e5842..7cf26a8030 100644 --- a/src/i18n/messages/all.es.json +++ b/src/i18n/messages/all.es.json @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Cargando paso siguiente", "i18nStrings.submitButtonLoadingAnnouncement": "Formulario de envío" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.fr.json b/src/i18n/messages/all.fr.json index f420434c39..703d559d7f 100644 --- a/src/i18n/messages/all.fr.json +++ b/src/i18n/messages/all.fr.json @@ -159,17 +159,17 @@ "i18nStrings.endDateLabel": "Date de fin", "i18nStrings.endTimeLabel": "Heure de fin", "i18nStrings.datePlaceholder": "JJ/MM/AAAA", - "i18nStrings.isoDatePlaceholder": "AAAA-MM-JJ", - "i18nStrings.slashedDatePlaceholder": "AAAA/MM/JJ", + "i18nStrings.isoDatePlaceholder": "JJ-MM-AAAA", + "i18nStrings.slashedDatePlaceholder": "JJ/MM/AAAA", "i18nStrings.timePlaceholder": "hh:mm:ss", - "i18nStrings.dateTimeConstraintText": "Pour la date, utilisez le format AAAA/MM/JJ. Pour l'heure, utilisez le format 24 heures.", + "i18nStrings.dateTimeConstraintText": "Pour la date, utilisez le format AAAA/MM/JJ. Pour l'heure, utilisez le format 24 heures.", "i18nStrings.dateConstraintText": "Pour la date, utilisez le format AAAA/MM/JJ.", "i18nStrings.slashedDateTimeConstraintText": "Pour la date, utilisez le format JJ/MM/AAAA. Pour l’heure, utilisez le format 24 heures.", - "i18nStrings.isoDateTimeConstraintText": "Pour la date, utilisez le format JJ-MM-AAAA. Pour l’heure, utilisez le format 24 heures.", + "i18nStrings.isoDateTimeConstraintText": "Pour la date, utilisez le format JJ/MM/AAAA. Pour l’heure, utilisez le format 24 heures.", "i18nStrings.slashedDateConstraintText": "Pour la date, utilisez le format JJ/MM/AAAA.", - "i18nStrings.isoDateConstraintText": "Pour la date, utilisez le format JJ-MM-AAAA.", + "i18nStrings.isoDateConstraintText": "Pour la date, utilisez le format JJ/MM/AAAA.", "i18nStrings.slashedMonthConstraintText": "Pour le mois, utilisez le format MM/AAAA.", - "i18nStrings.isoMonthConstraintText": "Pour le mois, utilisez le format MM-AAAA.", + "i18nStrings.isoMonthConstraintText": "Pour le mois, utilisez le format MM/AAAA.", "i18nStrings.monthConstraintText": "Pour le mois, utilisez le format AAAA/MM.", "i18nStrings.errorIconAriaLabel": "Erreur", "i18nStrings.renderSelectedAbsoluteRangeAriaLive": "Plage sélectionnée de {startDate} à {endDate}", @@ -195,7 +195,7 @@ "file-token-group": { "i18nStrings.limitShowFewer": "Afficher moins", "i18nStrings.limitShowMore": "Afficher plus", - "i18nStrings.removeFileAriaLabel": "Supprimer le fichier {fileIndex}, {fileName}", + "i18nStrings.removeFileAriaLabel": "Supprimer le fichier {fileIndex}, {fileName}", "i18nStrings.errorIconAriaLabel": "Erreur", "i18nStrings.warningIconAriaLabel": "Avertissement" }, @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Chargement de l'étape suivante", "i18nStrings.submitButtonLoadingAnnouncement": "Soumission du formulaire" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.id.json b/src/i18n/messages/all.id.json index 1b9efc8652..ee87114e52 100644 --- a/src/i18n/messages/all.id.json +++ b/src/i18n/messages/all.id.json @@ -159,8 +159,8 @@ "i18nStrings.endDateLabel": "Tanggal berakhir", "i18nStrings.endTimeLabel": "Waktu berakhir", "i18nStrings.datePlaceholder": "HH-BB-TTTT", - "i18nStrings.isoDatePlaceholder": "TTTT-BB-HH", - "i18nStrings.slashedDatePlaceholder": "TTTT/BB/HH", + "i18nStrings.isoDatePlaceholder": "HH-BB-TTTT", + "i18nStrings.slashedDatePlaceholder": "HH/BB/TTTT", "i18nStrings.timePlaceholder": "jj:mm:dd", "i18nStrings.dateTimeConstraintText": "Untuk tanggal, gunakan HH/BB/TTTT. Untuk waktu, gunakan format 24 jam.", "i18nStrings.dateConstraintText": "Untuk tanggal, gunakan HH/BB/TTTT.", @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Memuat langkah berikutnya", "i18nStrings.submitButtonLoadingAnnouncement": "Mengirimkan formulir" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.it.json b/src/i18n/messages/all.it.json index d9da27dfa2..09851f3db3 100644 --- a/src/i18n/messages/all.it.json +++ b/src/i18n/messages/all.it.json @@ -159,8 +159,8 @@ "i18nStrings.endDateLabel": "Data di fine", "i18nStrings.endTimeLabel": "Ora di fine", "i18nStrings.datePlaceholder": "GG/MM/AAAA", - "i18nStrings.isoDatePlaceholder": "AAAA-MM-GG", - "i18nStrings.slashedDatePlaceholder": "AAAA/MM/GG", + "i18nStrings.isoDatePlaceholder": "GG-MM-AAAA", + "i18nStrings.slashedDatePlaceholder": "GG/MM/AAAA", "i18nStrings.timePlaceholder": "hh:mm:ss", "i18nStrings.dateTimeConstraintText": "Per la data, utilizza il formato GG/MM/AAAA. Per l'ora, usa il formato 24 ore.", "i18nStrings.dateConstraintText": "Per la data, utilizza il formato GG/MM/AAAA.", @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Caricamento della fase successiva", "i18nStrings.submitButtonLoadingAnnouncement": "Modulo di invio" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.ja.json b/src/i18n/messages/all.ja.json index 3fdbc5b7a9..4246f10256 100644 --- a/src/i18n/messages/all.ja.json +++ b/src/i18n/messages/all.ja.json @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "次のステップをロード中", "i18nStrings.submitButtonLoadingAnnouncement": "フォーム送信" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.ko.json b/src/i18n/messages/all.ko.json index 170e290cea..dd02bc86c5 100644 --- a/src/i18n/messages/all.ko.json +++ b/src/i18n/messages/all.ko.json @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "다음 단계 로드 중", "i18nStrings.submitButtonLoadingAnnouncement": "양식 제출 중" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.pt-BR.json b/src/i18n/messages/all.pt-BR.json index 4065565c2c..510d7de7ba 100644 --- a/src/i18n/messages/all.pt-BR.json +++ b/src/i18n/messages/all.pt-BR.json @@ -165,11 +165,11 @@ "i18nStrings.dateTimeConstraintText": "Para a data, use o formato AAAA/MM/DD. Para o horário, use o formato de 24 horas.", "i18nStrings.dateConstraintText": "Para data, use AAAA/MM/DD.", "i18nStrings.slashedDateTimeConstraintText": "Para a data, use o formato AAAA/MM/DD. Para o horário, use o formato de 24 horas.", - "i18nStrings.isoDateTimeConstraintText": "Para a data, use o formato AAAA-MM-DD. Para o horário, use o formato de 24 horas.", + "i18nStrings.isoDateTimeConstraintText": "Para a data, use o formato AAAA/MM/DD. Para o horário, use o formato de 24 horas.", "i18nStrings.slashedDateConstraintText": "Para data, use AAAA/MM/DD.", - "i18nStrings.isoDateConstraintText": "Para data, use AAAA-MM-DD.", + "i18nStrings.isoDateConstraintText": "Para data, use AAAA/MM/DD.", "i18nStrings.slashedMonthConstraintText": "Para mês, use AAAA/MM.", - "i18nStrings.isoMonthConstraintText": "Para mês, use AAAA-MM.", + "i18nStrings.isoMonthConstraintText": "Para mês, use AAAA/MM.", "i18nStrings.monthConstraintText": "Para mês, use AAAA/MM.", "i18nStrings.errorIconAriaLabel": "Erro", "i18nStrings.renderSelectedAbsoluteRangeAriaLive": "Intervalo selecionado de {startDate} a {endDate}", @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Carregando próxima etapa", "i18nStrings.submitButtonLoadingAnnouncement": "Enviando formulário" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.th.json b/src/i18n/messages/all.th.json index 91de409140..247710388e 100644 --- a/src/i18n/messages/all.th.json +++ b/src/i18n/messages/all.th.json @@ -361,4 +361,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "กำลังโหลดขั้นตอนถัดไป", "i18nStrings.submitButtonLoadingAnnouncement": "กำลังส่งแบบฟอร์ม" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.tr.json b/src/i18n/messages/all.tr.json index 435f4b57a1..8975c89237 100644 --- a/src/i18n/messages/all.tr.json +++ b/src/i18n/messages/all.tr.json @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Sonraki adım yükleniyor", "i18nStrings.submitButtonLoadingAnnouncement": "Form gönderiliyor" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.zh-CN.json b/src/i18n/messages/all.zh-CN.json index 236b92e571..4114cb7ed3 100644 --- a/src/i18n/messages/all.zh-CN.json +++ b/src/i18n/messages/all.zh-CN.json @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "正在加载下一步", "i18nStrings.submitButtonLoadingAnnouncement": "正在提交表单" } -} +} \ No newline at end of file diff --git a/src/i18n/messages/all.zh-TW.json b/src/i18n/messages/all.zh-TW.json index d5c8489302..48b9c97e87 100644 --- a/src/i18n/messages/all.zh-TW.json +++ b/src/i18n/messages/all.zh-TW.json @@ -483,4 +483,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "載入下一個步驟", "i18nStrings.submitButtonLoadingAnnouncement": "提交表單" } -} +} \ No newline at end of file diff --git a/src/input/internal.tsx b/src/input/internal.tsx index 26a90bbe60..57c81b79f3 100644 --- a/src/input/internal.tsx +++ b/src/input/internal.tsx @@ -53,6 +53,7 @@ export interface InternalInputProps __injectAnalyticsComponentMetadata?: boolean; __skipNativeAttributesWarnings?: SkipWarnings; __inlineLabelText?: string; + __fullWidth?: boolean; } function InternalInput( @@ -95,6 +96,7 @@ function InternalInput( __injectAnalyticsComponentMetadata, __skipNativeAttributesWarnings, __inlineLabelText, + __fullWidth, style, ...rest }: InternalInputProps, @@ -226,11 +228,18 @@ function InternalInput(
    )} {__inlineLabelText ? ( -
    +
    -
    {mainInput}
    +
    + {mainInput} +
    ) : ( mainInput diff --git a/src/input/styles.scss b/src/input/styles.scss index b2b350f582..da5af22277 100644 --- a/src/input/styles.scss +++ b/src/input/styles.scss @@ -207,10 +207,18 @@ .inline-label-trigger-wrapper { @include forms.inline-label-trigger-wrapper; + + &-full-width { + inline-size: 100%; + } } .inline-label-wrapper { @include forms.inline-label-wrapper; + + &-full-width { + inline-size: 100%; + } } .inline-label { diff --git a/src/pagination/interfaces.ts b/src/pagination/interfaces.ts index 17aae7db40..6a69600e0c 100644 --- a/src/pagination/interfaces.ts +++ b/src/pagination/interfaces.ts @@ -48,6 +48,9 @@ export interface PaginationProps { * @i18n */ ariaLabels?: PaginationProps.Labels; + /** + * @i18n + */ i18nStrings?: PaginationProps.I18nStrings; /** @@ -85,8 +88,12 @@ export namespace PaginationProps { } export interface I18nStrings { + /** @i18n */ + jumpToPageInputLabel?: string; + /** @i18n */ jumpToPageError?: string; - jumpToPageLabel?: string; + /** @i18n */ + jumpToPageLoadingText?: string; } export interface ChangeDetail { diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index a0d4a18c3c..7cc2d0fb5e 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -25,19 +25,6 @@ import { getPaginationState, range } from './utils'; import styles from './styles.css.js'; -const defaultAriaLabels: Required = { - nextPageLabel: '', - paginationLabel: '', - previousPageLabel: '', - jumpToPageButton: '', - pageLabel: pageNumber => `${pageNumber}`, -}; - -const defaultI18nStrings: Required = { - jumpToPageLabel: '', - jumpToPageError: '', -}; - interface PageButtonProps { className?: string; ariaLabel: string; @@ -135,12 +122,9 @@ const InternalPagination = React.forwardRef( 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, () => ({ @@ -155,18 +139,16 @@ const InternalPagination = React.forwardRef( prevLoadingRef.current = jumpToPage?.loading; }, [jumpToPage?.loading, currentPageIndex]); - const paginationLabel = ariaLabels?.paginationLabel; - const nextPageLabel = i18n('ariaLabels.nextPageLabel', ariaLabels?.nextPageLabel); - const previousPageLabel = i18n('ariaLabels.previousPageLabel', ariaLabels?.previousPageLabel); + 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; + i18n('ariaLabels.pageLabel', ariaLabels?.pageLabel, format => pageNumber => format({ pageNumber })) ?? (() => ''); + + const jumpToPageLabel = i18n('i18nStrings.jumpToPageInputLabel', i18nStrings?.jumpToPageInputLabel) ?? ''; + const jumpToPageButtonLabel = i18n('ariaLabels.jumpToPageButtonLabel', ariaLabels?.jumpToPageButton) ?? ''; + const jumpToPageError = i18n('i18nStrings.jumpToPageError', i18nStrings?.jumpToPageError) ?? ''; + const jumpToPageLoadingText = i18n('i18nStrings.jumpToPageLoadingText', i18nStrings?.jumpToPageLoadingText) ?? ''; function handlePrevPageClick(requestedPageIndex: number) { handlePageClick(requestedPageIndex); @@ -220,19 +202,6 @@ const InternalPagination = React.forwardRef( } }; - // 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(); @@ -247,8 +216,8 @@ const InternalPagination = React.forwardRef( iconName="arrow-right" variant="icon" loading={jumpToPage?.loading} - loadingText={loadingText} - ariaLabel={jumpToPage?.loading ? loadingText : (jumpToPageButtonLabel ?? defaultAriaLabels.jumpToPageButton)} + loadingText={jumpToPageLoadingText} + ariaLabel={jumpToPage?.loading ? jumpToPageLoadingText : jumpToPageButtonLabel} onClick={() => handleJumpToPageClick(Number(jumpToPageValue))} disabled={!jumpToPageValue || Number(jumpToPageValue) === currentPageIndex} /> @@ -264,7 +233,7 @@ const InternalPagination = React.forwardRef( setPopoverVisible(false)} + onBlur={() => setHasError(false)} onKeyDown={e => { if (e.detail.keyCode === 13 && jumpToPageValue && Number(jumpToPageValue) !== currentPageIndex) { handleJumpToPageClick(Number(jumpToPageValue)); @@ -348,11 +318,11 @@ const InternalPagination = React.forwardRef( setPopoverVisible(detail.visible)} + __onVisibleChange={({ detail }) => !detail.visible && setHasError(false)} > {jumpToPageButton} diff --git a/src/pagination/styles.scss b/src/pagination/styles.scss index 5fbda0980c..32e020c5ba 100644 --- a/src/pagination/styles.scss +++ b/src/pagination/styles.scss @@ -91,11 +91,6 @@ inline-size: 87px; margin-block-start: -0.6em; overflow: visible; - - // stylelint-disable-next-line selector-max-universal - > * > * { - inline-size: 100%; - } } } From 1cbd7334c263cd958adc2543c78371a09c86fcfc Mon Sep 17 00:00:00 2001 From: srungta08 Date: Tue, 17 Feb 2026 14:14:22 +0100 Subject: [PATCH 25/35] refactor: move theme validation after render setup in tests (#4253) --- .../__tests__/runtime-drawers.test.tsx | 107 +++++++----------- 1 file changed, 42 insertions(+), 65 deletions(-) diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index 3bfda7c950..98208be33c 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -1135,10 +1135,6 @@ describe('toolbar mode only features', () => { mountContent: container => (container.textContent = 'global drawer content 1'), }); - if (await validateThemeCompatibility(theme, )) { - return; - } - const renderProps = await renderComponent(); const { globalDrawersWrapper } = renderProps; @@ -1166,9 +1162,6 @@ describe('toolbar mode only features', () => { preserveInactiveContent: true, }); - if (await validateThemeCompatibility(theme, )) { - return; - } const renderProps = await renderComponent(); const { globalDrawersWrapper } = renderProps; @@ -1205,10 +1198,6 @@ describe('toolbar mode only features', () => { preserveInactiveContent: true, }); - if (await validateThemeCompatibility(theme, )) { - return; - } - const renderProps = await renderComponent(); const { globalDrawersWrapper } = renderProps; @@ -1232,7 +1221,11 @@ describe('toolbar mode only features', () => { test(`closes a drawer when closeDrawer is called (${type} drawer)`, async () => { registerDrawer({ ...drawerDefaults, resizable: true }); - if (await validateThemeCompatibility(theme, )) { + const { wrapper } = await renderComponent(); + + await delay(); + + if (isNotRefreshToolbarTheme(theme)) { return; } @@ -1266,10 +1259,6 @@ describe('toolbar mode only features', () => { }); test('should render trigger buttons for global drawers even if local drawers are not present', async () => { - if (await validateThemeCompatibility(theme, )) { - return; - } - const renderProps = await renderComponent(); registerDrawer({ @@ -1294,12 +1283,16 @@ describe('toolbar mode only features', () => { onToggle: event => onToggle(event.detail), }); + const renderProps = await renderComponent(); ======= - if (await validateThemeCompatibility(theme, )) { + const renderProps = await renderComponent(); + + await delay(); + + if (isNotRefreshToolbarTheme(theme)) { return; } ->>>>>>> 7bdbb71cb (test: add classic theme tests for drawer filtering test (#4245)) - const renderProps = await renderComponent(); +>>>>>>> 4fa2b7697 (refactor: move theme validation after render setup in tests (#4253)) await delay(); @@ -1395,7 +1388,11 @@ describe('toolbar mode only features', () => { mountContent: container => (container.textContent = 'global drawer content 2'), }); - if (await validateThemeCompatibility(theme, )) { + const { wrapper, globalDrawersWrapper } = await renderComponent(); + + await delay(); + + if (isNotRefreshToolbarTheme(theme)) { return; } @@ -1422,7 +1419,11 @@ describe('toolbar mode only features', () => { mountContent: container => (container.textContent = 'global drawer content 1'), }); - if (await validateThemeCompatibility(theme, )) { + const { wrapper, globalDrawersWrapper } = await renderComponent(); + + await delay(); + + if (isNotRefreshToolbarTheme(theme)) { return; } @@ -1452,24 +1453,6 @@ describe('toolbar mode only features', () => { trigger: undefined, }); - if ( - await validateThemeCompatibility( - theme, - - - - } - /> - ) - ) { - return; - } - const { globalDrawersWrapper, getByTestId } = await renderComponent( { test('should not render a trigger button if registered drawer does not have a trigger prop', async () => { awsuiPlugins.appLayout.registerDrawer({ ...drawerDefaults, trigger: undefined }); - if (await validateThemeCompatibility(theme, )) { + const { wrapper } = await renderComponent(); + + await delay(); + + if (isNotRefreshToolbarTheme(theme)) { return; } @@ -1560,10 +1547,6 @@ describe('toolbar mode only features', () => { describe('dynamically registered drawers with defaultActive: true', () => { test('should open if there are already open local drawer on the page', async () => { - if (await validateThemeCompatibility(theme, )) { - return; - } - const { wrapper, globalDrawersWrapper } = await renderComponent(); wrapper.findDrawerTriggerById('security')!.click(); @@ -1599,10 +1582,6 @@ describe('toolbar mode only features', () => { }); test('should not open if there are already global drawers opened by user action on the page', async () => { - if (await validateThemeCompatibility(theme, )) { - return; - } - const { wrapper, globalDrawersWrapper } = await renderComponent(); wrapper.findDrawerTriggerById('security')!.click(); @@ -1638,10 +1617,6 @@ describe('toolbar mode only features', () => { }); test('should not open if the maximum number (2) of global drawers is already open on the page', async () => { - if (await validateThemeCompatibility(theme, )) { - return; - } - const { wrapper, globalDrawersWrapper } = await renderComponent(); wrapper.findDrawerTriggerById('security')!.click(); @@ -1688,10 +1663,6 @@ describe('toolbar mode only features', () => { }); test('should open global bottom drawer by default when defaultActive is set', async () => { - if (await validateThemeCompatibility(theme, )) { - return; - } - const { globalDrawersWrapper } = await renderComponent(); awsuiWidgetPlugins.registerBottomDrawer({ @@ -2129,7 +2100,11 @@ describe('toolbar mode only features', () => { awsuiWidgetPlugins.registerLeftDrawer({ ...drawerDefaults, id: 'test2' }); awsuiWidgetPlugins.registerBottomDrawer({ ...drawerDefaults, id: 'bottom' }); - if (await validateThemeCompatibility(theme, )) { + const { globalDrawersWrapper } = await renderComponent(); + + await delay(); + + if (isNotRefreshToolbarTheme(theme)) { return; } @@ -2189,9 +2164,6 @@ describe('toolbar mode only features', () => { }); test('calls onResize handler for bottom drawer', async () => { - if (await validateThemeCompatibility(theme, )) { - return; - } const { wrapper, globalDrawersWrapper, debug } = await renderComponent(); const onResize = jest.fn(); awsuiWidgetPlugins.registerBottomDrawer({ @@ -2274,8 +2246,11 @@ describe('toolbar mode only features', () => { test('assigns correct ARIA roles when mixing global and regular drawers', async () => { registerGlobalDrawers(2); + const { wrapper } = await renderComponent(); + + await delay(); - if (await validateThemeCompatibility(theme, )) { + if (isNotRefreshToolbarTheme(theme)) { return; } const { wrapper } = await renderComponent(); @@ -2306,7 +2281,12 @@ describe('toolbar mode only features', () => { test('assigns menuitemcheckbox role to global drawers in overflow menu', async () => { registerGlobalDrawers(3); - if (await validateThemeCompatibility(theme, )) { + // In mobile view, two drawers are visible in the toolbar, the others are placed in the overflow menu + const { wrapper, globalDrawersWrapper } = await renderComponent(); + + await delay(); + + if (isNotRefreshToolbarTheme(theme)) { return; } @@ -2344,9 +2324,6 @@ describe('toolbar mode only features', () => { type: 'global', isExpandable: true, }); - if (await validateThemeCompatibility(theme, )) { - return; - } const { wrapper, globalDrawersWrapper } = await renderComponent(); await delay(); From 5e44b027b0dc9a2d2c07ecc49d597699423af2e0 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Fri, 9 Jan 2026 16:20:27 -0800 Subject: [PATCH 26/35] feat: add pagination - jump to page, update i18n, snapshots --- src/i18n/messages/all.en.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index d9bb541301..3756882d94 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -159,8 +159,6 @@ "i18nStrings.endDateLabel": "End date", "i18nStrings.endTimeLabel": "End time", "i18nStrings.datePlaceholder": "YYYY-MM-DD", - "i18nStrings.isoDatePlaceholder": "YYYY-MM-DD", - "i18nStrings.slashedDatePlaceholder": "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.", From 3056ae673f4859c7fde895c12056b083afb56bfe Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Thu, 15 Jan 2026 15:42:41 -0800 Subject: [PATCH 27/35] Remove unrelated i18n changes --- src/i18n/messages/all.en.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 3756882d94..ecee4e6c7f 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.", From ca2317bcef234f8229326e197631822c9498fb27 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Fri, 9 Jan 2026 16:20:27 -0800 Subject: [PATCH 28/35] feat: add pagination - jump to page, update i18n, snapshots --- src/i18n/messages/all.en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index ecee4e6c7f..3756882d94 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.", From 86062867efa279033413f64623c7d6c60f02f55c Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Wed, 11 Mar 2026 18:02:30 -0700 Subject: [PATCH 29/35] fix tests --- .../__snapshots__/design-tokens.test.ts.snap | 88 +++++++++---------- src/pagination/internal.tsx | 5 +- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/design-tokens.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/design-tokens.test.ts.snap index b5f01f2e0a..63c71653c2 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/design-tokens.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/design-tokens.test.ts.snap @@ -2517,23 +2517,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -5370,23 +5370,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -8223,23 +8223,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -11076,23 +11076,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -13929,23 +13929,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -16782,23 +16782,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -19635,23 +19635,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "normal", + "$value": "0em", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -22509,7 +22509,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -25362,7 +25362,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -28215,7 +28215,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -31068,7 +31068,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -33921,7 +33921,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -36774,7 +36774,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -39627,7 +39627,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -42480,7 +42480,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -45333,7 +45333,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "normal", + "$value": "0em", }, "line-height-body-m": { "$description": "The default line height for regular body text.", diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index 7cc2d0fb5e..e2c29288cd 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -143,7 +143,8 @@ const InternalPagination = React.forwardRef( 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 })) ?? (() => ''); + i18n('ariaLabels.pageLabel', ariaLabels?.pageLabel, format => pageNumber => format({ pageNumber })) ?? + ((pageNumber: number) => `${pageNumber}`); const jumpToPageLabel = i18n('i18nStrings.jumpToPageInputLabel', i18nStrings?.jumpToPageInputLabel) ?? ''; const jumpToPageButtonLabel = i18n('ariaLabels.jumpToPageButtonLabel', ariaLabels?.jumpToPageButton) ?? ''; @@ -318,7 +319,7 @@ const InternalPagination = React.forwardRef( Date: Wed, 11 Mar 2026 18:12:15 -0700 Subject: [PATCH 30/35] update snapshots --- .../snapshot-tests/__snapshots__/documenter.test.ts.snap | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index ece76561e6..61e0f159eb 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -19008,6 +19008,7 @@ from changing page before items are loaded.", "type": "boolean", }, { + "i18nTag": true, "inlineType": { "name": "PaginationProps.I18nStrings", "properties": [ @@ -19017,7 +19018,12 @@ from changing page before items are loaded.", "type": "string", }, { - "name": "jumpToPageLabel", + "name": "jumpToPageInputLabel", + "optional": true, + "type": "string", + }, + { + "name": "jumpToPageLoadingText", "optional": true, "type": "string", }, From 6e2f4886e57626ccc4296728a66a3228c8fb5b91 Mon Sep 17 00:00:00 2001 From: Gethin Webster Date: Mon, 16 Mar 2026 14:17:02 +0100 Subject: [PATCH 31/35] Fix snapshots --- .../__snapshots__/design-tokens.test.ts.snap | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/design-tokens.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/design-tokens.test.ts.snap index 63c71653c2..b5f01f2e0a 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/design-tokens.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/design-tokens.test.ts.snap @@ -2517,23 +2517,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -5370,23 +5370,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -8223,23 +8223,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -11076,23 +11076,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -13929,23 +13929,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -16782,23 +16782,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -19635,23 +19635,23 @@ exports[`Design tokens artifacts Design tokens JSON for classic matches the snap }, "letter-spacing-heading-l": { "$description": "The default letter spacing for h2s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-m": { "$description": "The default letter spacing for h3s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-s": { "$description": "The default letter spacing for h4s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xl": { "$description": "The default letter spacing for h1s.", - "$value": "0em", + "$value": "normal", }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -22509,7 +22509,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -25362,7 +25362,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -28215,7 +28215,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -31068,7 +31068,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -33921,7 +33921,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -36774,7 +36774,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -39627,7 +39627,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -42480,7 +42480,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", @@ -45333,7 +45333,7 @@ exports[`Design tokens artifacts Design tokens JSON for visual-refresh matches t }, "letter-spacing-heading-xs": { "$description": "The default letter spacing for h5s.", - "$value": "0em", + "$value": "normal", }, "line-height-body-m": { "$description": "The default line height for regular body text.", From dffb4a5d58c57772033d156a4e9671958549a8b1 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Mon, 23 Mar 2026 22:07:16 -0700 Subject: [PATCH 32/35] fix: prevent user input of comma in jump to page input --- src/pagination/internal.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index e2c29288cd..2ad24870b7 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -309,6 +309,10 @@ const InternalPagination = React.forwardRef( onChange={handleInputChange} onBlur={() => setHasError(false)} onKeyDown={e => { + if (e.detail.key === ',') { + e.preventDefault(); + return; + } if (e.detail.keyCode === 13 && jumpToPageValue && Number(jumpToPageValue) !== currentPageIndex) { handleJumpToPageClick(Number(jumpToPageValue)); } From 16d5ea3d088e54068f47448dd18fd039b0678ebe Mon Sep 17 00:00:00 2001 From: Gethin Webster Date: Wed, 25 Mar 2026 09:12:33 +0100 Subject: [PATCH 33/35] Round non-integer page number inputs --- src/pagination/__tests__/pagination.test.tsx | 17 +++++++++++++++++ src/pagination/internal.tsx | 16 ++++------------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/pagination/__tests__/pagination.test.tsx b/src/pagination/__tests__/pagination.test.tsx index a764dfd309..b3ae3d30f8 100644 --- a/src/pagination/__tests__/pagination.test.tsx +++ b/src/pagination/__tests__/pagination.test.tsx @@ -392,6 +392,23 @@ describe('jump to page', () => { expect(wrapper.findJumpToPageInput()!.findNativeInput().getElement()).toHaveValue(1); }); + test('should round down to closest integer', () => { + const onChange = jest.fn(); + const { wrapper } = renderPagination( + + ); + + wrapper.findJumpToPageInput()!.setInputValue('2.7'); + wrapper.findJumpToPageButton()!.click(); + + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + detail: { currentPageIndex: 2 }, + }) + ); + expect(wrapper.findJumpToPageInput()!.findNativeInput().getElement()).toHaveValue(2); + }); + test('should show error and navigate to last page when input exceeds pagesCount', () => { const onChange = jest.fn(); const { wrapper } = renderPagination( diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index 2ad24870b7..b3964eaa25 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -174,19 +174,15 @@ const InternalPagination = React.forwardRef( } function handleJumpToPageClick(requestedPageIndex: number) { - if (requestedPageIndex < 1) { - handlePageClick(1); - jumpToPageInputRef.current?.focus(); - return; - } + const adjustedIndex = Math.max(1, Math.floor(requestedPageIndex)); if (openEnd) { // Open-end: always navigate, parent will handle async loading - handlePageClick(requestedPageIndex); + handlePageClick(adjustedIndex); } else { // Closed-end: validate range - if (requestedPageIndex >= 1 && requestedPageIndex <= pagesCount) { - handlePageClick(requestedPageIndex); + if (adjustedIndex >= 1 && adjustedIndex <= pagesCount) { + handlePageClick(adjustedIndex); } else { // Out of range - set error and navigate to last page handlePageClick(pagesCount, true); @@ -309,10 +305,6 @@ const InternalPagination = React.forwardRef( onChange={handleInputChange} onBlur={() => setHasError(false)} onKeyDown={e => { - if (e.detail.key === ',') { - e.preventDefault(); - return; - } if (e.detail.keyCode === 13 && jumpToPageValue && Number(jumpToPageValue) !== currentPageIndex) { handleJumpToPageClick(Number(jumpToPageValue)); } From 476942e61a0e42fb2970c186be87b2102b179042 Mon Sep 17 00:00:00 2001 From: Gethin Webster Date: Wed, 1 Apr 2026 09:16:37 +0200 Subject: [PATCH 34/35] Address comments --- pages/pagination/permutations.page.tsx | 6 - pages/table/jump-to-page-closed.page.tsx | 152 +++++++++++------------ src/pagination/internal.tsx | 8 +- 3 files changed, 77 insertions(+), 89 deletions(-) diff --git a/pages/pagination/permutations.page.tsx b/pages/pagination/permutations.page.tsx index 34c8afff9c..a3bba70f5a 100644 --- a/pages/pagination/permutations.page.tsx +++ b/pages/pagination/permutations.page.tsx @@ -17,11 +17,6 @@ const paginationLabels: PaginationProps.Labels = { jumpToPageButton: 'Go to page', }; -const paginationI18nStrings: PaginationProps.I18nStrings = { - jumpToPageInputLabel: 'Page', - jumpToPageError: 'Enter a valid page number', -}; - const permutations = createPermutations([ { currentPageIndex: [7], @@ -34,7 +29,6 @@ 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-closed.page.tsx b/pages/table/jump-to-page-closed.page.tsx index e05ac153d6..1bed4c139d 100644 --- a/pages/table/jump-to-page-closed.page.tsx +++ b/pages/table/jump-to-page-closed.page.tsx @@ -13,7 +13,7 @@ import { generateItems, Instance } from './generate-data'; const allItems = generateItems(100); const PAGE_SIZE = 10; -function JumpToPageClosedContent() { +export default function JumpToPageClosedExample() { const [currentPageIndex, setCurrentPageIndex] = useState(1); const totalPages = Math.ceil(allItems.length / PAGE_SIZE); @@ -22,88 +22,82 @@ function JumpToPageClosedContent() { 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={ - +
    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={{}} - /> - } - /> - ); -} - -export default function JumpToPageClosedExample() { - return ( - - + }} + stickyColumnsPreference={{ + firstColumns: { + title: 'Stick first column(s)', + description: 'Keep the first column(s) visible while horizontally scrolling the table content.', + options: [ + { label: 'None', value: 0 }, + { label: 'First column', value: 1 }, + { label: 'First two columns', value: 2 }, + ], + }, + lastColumns: { + title: 'Stick last column', + description: 'Keep the last column visible while horizontally scrolling the table content.', + options: [ + { label: 'None', value: 0 }, + { label: 'Last column', value: 1 }, + ], + }, + }} + /> + } + items={currentItems} + pagination={ + setCurrentPageIndex(detail.currentPageIndex)} + jumpToPage={{}} + /> + } + /> ); } diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index b3964eaa25..eaf9e246cc 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -17,6 +17,7 @@ import { getBaseProps } from '../internal/base-component'; import { useTableComponentsContext } from '../internal/context/table-component-context'; import { fireNonCancelableEvent, NonCancelableCustomEvent } from '../internal/events'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; +import { usePrevious } from '../internal/hooks/use-previous'; import InternalPopover from '../popover/internal'; import InternalSpaceBetween from '../space-between/internal'; import { GeneratedAnalyticsMetadataPaginationClick } from './analytics-metadata/interfaces'; @@ -120,7 +121,7 @@ const InternalPagination = React.forwardRef( 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 previousLoading = usePrevious(jumpToPage?.loading); const jumpToPageInputRef = useRef(null); const [hasError, setHasError] = useState(false); @@ -133,11 +134,10 @@ const InternalPagination = React.forwardRef( // Sync input with currentPageIndex after loading completes React.useEffect(() => { - if (prevLoadingRef.current && !jumpToPage?.loading) { + if (previousLoading && !jumpToPage?.loading) { setJumpToPageValue(String(currentPageIndex)); } - prevLoadingRef.current = jumpToPage?.loading; - }, [jumpToPage?.loading, currentPageIndex]); + }, [previousLoading, jumpToPage?.loading, currentPageIndex]); const paginationLabel = ariaLabels?.paginationLabel ?? ''; const nextPageLabel = i18n('ariaLabels.nextPageLabel', ariaLabels?.nextPageLabel) ?? ''; From 250f8ecb358bbce087aa426d59159ed5603a7ef9 Mon Sep 17 00:00:00 2001 From: Michelle Nguyen Date: Thu, 2 Apr 2026 15:58:11 -0700 Subject: [PATCH 35/35] fix: only mount error popover when visible, add content assertion --- src/pagination/__tests__/pagination.test.tsx | 11 +++++------ src/pagination/internal.tsx | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/pagination/__tests__/pagination.test.tsx b/src/pagination/__tests__/pagination.test.tsx index b3ae3d30f8..f4ee7f7910 100644 --- a/src/pagination/__tests__/pagination.test.tsx +++ b/src/pagination/__tests__/pagination.test.tsx @@ -455,8 +455,9 @@ describe('jump to page', () => { ref.current?.setError(true); rerender(); - // Error popover should be visible + // Error popover should be visible with content expect(wrapper.findJumpToPagePopover()).not.toBeNull(); + expect(wrapper.findJumpToPagePopover()!.findContent()).not.toBeNull(); }); test('should clear error when user types in input', () => { @@ -538,7 +539,7 @@ describe('jump to page', () => { }); describe('open-end error handling', () => { - test('should not show error popover content while loading in open-end mode', () => { + test('should not show error popover while loading in open-end mode', () => { const ref = React.createRef(); const { wrapper, rerender } = renderPagination( @@ -549,10 +550,8 @@ describe('jump to page', () => { ); - // Popover wrapper exists but content should not be visible while loading - const popover = wrapper.findJumpToPagePopover(); - expect(popover).not.toBeNull(); - expect(popover!.findContent()).toBeNull(); + // Popover should not be mounted while loading + expect(wrapper.findJumpToPagePopover()).toBeNull(); }); test('should show error popover after loading completes in open-end mode', () => { diff --git a/src/pagination/internal.tsx b/src/pagination/internal.tsx index eaf9e246cc..62f07af47c 100644 --- a/src/pagination/internal.tsx +++ b/src/pagination/internal.tsx @@ -311,11 +311,11 @@ const InternalPagination = React.forwardRef( }} /> - {hasError ? ( + {hasError && !jumpToPage?.loading ? (