From 0904e66e1118e09b1b8621cd9bfb9b252616f531 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Wed, 10 Jun 2026 11:17:28 +0200 Subject: [PATCH 1/6] Allow LoadingContainer to forward className to its wrapper --- src/components/loading-container/LoadingContainer.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/loading-container/LoadingContainer.tsx b/src/components/loading-container/LoadingContainer.tsx index c15a8123..61d8152f 100644 --- a/src/components/loading-container/LoadingContainer.tsx +++ b/src/components/loading-container/LoadingContainer.tsx @@ -25,6 +25,7 @@ import { LoadingContext } from '~common/components/LoadingContext'; import { screenReaderOnly } from '~common/helpers/styles'; export interface LoadingContainerProps { + className?: string; isLoading: boolean; /** * Specify what the screen reader announces once isLoading switches to `false` @@ -52,14 +53,16 @@ export interface LoadingContainerProps { * Both status messages can be specified. */ export function LoadingContainer(props: PropsWithChildren) { - const { children, isLoading, loadedMessage, loadingMessage } = props; + const { className, children, isLoading, loadedMessage, loadingMessage } = props; const loadingContextValue = useMemo(() => ({ isLoading }), [isLoading]); return ( <> -
{children}
+
+ {children} +
@@ -83,7 +86,7 @@ export function LoadingContainer(props: PropsWithChildren ); } -export const ScreenReaderOnlyLive = styled.span` +const ScreenReaderOnlyLive = styled.span` ${screenReaderOnly}; `; ScreenReaderOnlyLive.displayName = 'ScreenReaderOnlyLive'; From 0a4e9797fa7a1537aa89d74beba9f7adfb017960 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Wed, 10 Jun 2026 18:13:28 +0200 Subject: [PATCH 2/6] Add isLoading to layout components --- i18n/keys.json | 16 +++ .../components/LoadingStateProvider.tsx | 38 ++++++ .../ScreenReaderOnlyLoadingStatus.tsx | 69 ++++++++++ src/components/layout/LayoutSlots.tsx | 126 +++++++++++++++++- .../loading-container/LoadingContainer.tsx | 39 ++---- stories/layout/Layout-stories.tsx | 21 +-- 6 files changed, 266 insertions(+), 43 deletions(-) create mode 100644 src/common/components/LoadingStateProvider.tsx create mode 100644 src/common/components/ScreenReaderOnlyLoadingStatus.tsx diff --git a/i18n/keys.json b/i18n/keys.json index 6ff1ae4a..a46d63c0 100644 --- a/i18n/keys.json +++ b/i18n/keys.json @@ -108,6 +108,22 @@ "defaultMessage": "(opens in new tab)", "description": "Screen reader-only text to indicate that the link will open in a new tab" }, + "page_content.default_loaded_message": { + "defaultMessage": "Page content loaded", + "description": "Default message to be announced by screen readers when the page content is loaded" + }, + "page_content.default_loading_message": { + "defaultMessage": "Loading page content", + "description": "Default message to be announced by screen readers when the page content is loading" + }, + "page_grid.default_loaded_message": { + "defaultMessage": "Page loaded", + "description": "Default message to be announced by screen readers when the page is loaded" + }, + "page_grid.default_loading_message": { + "defaultMessage": "Loading page", + "description": "Default message to be announced by screen readers when the page is loading" + }, "pagination.next": { "defaultMessage": "Next", "description": "Label for the pagination button that takes you to the next page" diff --git a/src/common/components/LoadingStateProvider.tsx b/src/common/components/LoadingStateProvider.tsx new file mode 100644 index 00000000..38099e13 --- /dev/null +++ b/src/common/components/LoadingStateProvider.tsx @@ -0,0 +1,38 @@ +/* + * Echoes React + * Copyright (C) 2023-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { ReactNode, useMemo } from 'react'; +import { isDefined } from '~common/helpers/types'; +import { LoadingContext } from './LoadingContext'; + +interface LoadingStateProviderProps { + children?: ReactNode; + isLoading?: boolean; +} + +export function LoadingStateProvider({ children, isLoading }: Readonly) { + const loadingContextValue = useMemo(() => ({ isLoading: Boolean(isLoading) }), [isLoading]); + + return isDefined(isLoading) ? ( + {children} + ) : ( + children + ); +} diff --git a/src/common/components/ScreenReaderOnlyLoadingStatus.tsx b/src/common/components/ScreenReaderOnlyLoadingStatus.tsx new file mode 100644 index 00000000..3163eb6a --- /dev/null +++ b/src/common/components/ScreenReaderOnlyLoadingStatus.tsx @@ -0,0 +1,69 @@ +/* + * Echoes React + * Copyright (C) 2023-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import styled from '@emotion/styled'; +import { FormattedMessage } from 'react-intl'; +import { screenReaderOnly } from '~common/helpers/styles'; +import { TextNode } from '~types/utils'; + +export interface ScreenReaderOnlyLoadingStatusProps { + isLoading: boolean; + /** + * Specify what the screen reader announces once isLoading switches to `false` + * + * @defaultValue "Content loaded" + */ + loadedMessage?: TextNode; + /** + * Specify what the screen reader announces when isLoading is `true` + * + * @defaultValue "Loading content" + */ + loadingMessage?: TextNode; +} + +export function ScreenReaderOnlyLoadingStatus(props: Readonly) { + const { isLoading, loadedMessage, loadingMessage } = props; + + return ( + + {isLoading + ? (loadingMessage ?? ( + + )) + : (loadedMessage ?? ( + + ))} + + ); +} + +const ScreenReaderOnlyLive = styled.span` + ${screenReaderOnly}; +`; +ScreenReaderOnlyLive.displayName = 'ScreenReaderOnlyLive'; diff --git a/src/components/layout/LayoutSlots.tsx b/src/components/layout/LayoutSlots.tsx index 365555e6..5bc40ab8 100644 --- a/src/components/layout/LayoutSlots.tsx +++ b/src/components/layout/LayoutSlots.tsx @@ -20,6 +20,11 @@ import styled from '@emotion/styled'; import { CSSProperties, forwardRef, PropsWithChildren } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { LoadingStateProvider } from '~common/components/LoadingStateProvider'; +import { ScreenReaderOnlyLoadingStatus } from '~common/components/ScreenReaderOnlyLoadingStatus'; +import { isDefined } from '~common/helpers/types'; +import { TextNode } from '~types/utils'; import { cssVar } from '~utils/design-tokens'; import { AsideSize, ContentGridArea, GlobalGridArea, PageGridArea, PageWidth } from './LayoutTypes'; @@ -92,15 +97,66 @@ StyledAsideLeft.displayName = 'StyledAside'; export interface PageGridProps { className?: string; + /** + * Setting this prop will make this component behave like a LoadingContainer. + * It will provide a LoadingContext that LoadingSkeletons can consume (automatically), + * and deal with screen readers. + * Customize the accessible status messages by specifying `loadedMessage` and `loadingMessage`. + */ + isLoading?: boolean; + /** + * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * @defaultValue Page loaded + */ + loadedMessage?: TextNode; + /** + * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * @defaultValue Loading page + */ + loadingMessage?: TextNode; width?: `${PageWidth}`; } export const PageGrid = forwardRef>( (props, ref) => { - const { children, width = 'default', ...restProps } = props; + const { + children, + isLoading, + loadedMessage, + loadingMessage, + width = 'default', + ...restProps + } = props; + return ( - - {children} + + + {children} + + + {isDefined(isLoading) && ( + + ) + } + loadingMessage={ + loadingMessage ?? ( + + ) + } + /> + )} ); }, @@ -136,13 +192,73 @@ const PAGE_WIDTH_STYLES: Record = { [PageWidth.fluid]: {}, }; -export const PageContent = styled.main` +export interface PageContentProps { + className?: string; + /** + * Setting this prop will make this component behave like a LoadingContainer. + * It will provide a LoadingContext that LoadingSkeletons can consume (automatically), + * and deal with screen readers. + * Customize the accessible status messages by specifying `loadedMessage` and `loadingMessage`. + */ + isLoading?: boolean; + /** + * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * @defaultValue Page content loaded + */ + loadedMessage?: TextNode; + /** + * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * @defaultValue Loading page content + */ + loadingMessage?: TextNode; +} + +export const PageContent = forwardRef>( + (props, ref) => { + const { children, isLoading, loadedMessage, loadingMessage, ...restProps } = props; + + return ( + <> + + {children} + + + {isDefined(isLoading) && ( + + ) + } + loadingMessage={ + loadingMessage ?? ( + + ) + } + /> + )} + + ); + }, +); +PageContent.displayName = 'PageContent'; + +const StyledPageContent = styled.main` grid-area: ${PageGridArea.main}; padding: ${cssVar('dimension-space-300')}; isolation: isolate; // Prevent content z-index values from escaping and overlapping a sticky PageHeader `; -PageContent.displayName = 'PageContent'; +StyledPageContent.displayName = 'StyledPageContent'; export const PageFooter = styled.footer` grid-area: ${PageGridArea.footer}; diff --git a/src/components/loading-container/LoadingContainer.tsx b/src/components/loading-container/LoadingContainer.tsx index 61d8152f..62408799 100644 --- a/src/components/loading-container/LoadingContainer.tsx +++ b/src/components/loading-container/LoadingContainer.tsx @@ -18,11 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import styled from '@emotion/styled'; import { PropsWithChildren, useMemo } from 'react'; -import { FormattedMessage } from 'react-intl'; import { LoadingContext } from '~common/components/LoadingContext'; -import { screenReaderOnly } from '~common/helpers/styles'; +import { ScreenReaderOnlyLoadingStatus } from '~common/components/ScreenReaderOnlyLoadingStatus'; export interface LoadingContainerProps { className?: string; @@ -59,34 +57,15 @@ export function LoadingContainer(props: PropsWithChildren return ( <> - -
- {children} -
-
+
+ {children} +
- - {isLoading - ? (loadingMessage ?? ( - - )) - : (loadedMessage ?? ( - - ))} - + ); } - -const ScreenReaderOnlyLive = styled.span` - ${screenReaderOnly}; -`; -ScreenReaderOnlyLive.displayName = 'ScreenReaderOnlyLive'; diff --git a/stories/layout/Layout-stories.tsx b/stories/layout/Layout-stories.tsx index 23b8235c..8cd784e0 100644 --- a/stories/layout/Layout-stories.tsx +++ b/stories/layout/Layout-stories.tsx @@ -36,6 +36,7 @@ import { IconSecurityFinding, Layout, LinkStandalone, + LoadingSkeleton, LogoSonarQubeServer, Text, TextInput, @@ -144,6 +145,8 @@ export const Default: Story = { aside: AsideSize.medium, banner: 'none', contentHeader: false, + pageLoading: true, + pageContentLoading: true, pageHeader: true, pageHeaderScrollBehavior: PageHeaderScrollBehavior.scroll, pageWidth: 'default', @@ -158,16 +161,18 @@ export const Default: Story = { {args.contentHeader} {args.aside} - + {args.pageHeader(args.pageHeaderScrollBehavior)} - +

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure - dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt - mollit anim id est laborum. + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud + exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute + irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia + deserunt mollit anim id est laborum. +

From e6110c4d49edfbdc0e554348aef28b89873bdc7b Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Fri, 12 Jun 2026 09:36:41 +0200 Subject: [PATCH 3/6] Add isLoading to layout header component --- i18n/keys.json | 8 ++ .../layout/header/common/HeaderBase.tsx | 73 ++++++++++++++----- .../layout/header/common/HeaderTypes.ts | 18 +++++ 3 files changed, 80 insertions(+), 19 deletions(-) diff --git a/i18n/keys.json b/i18n/keys.json index a46d63c0..07cb21da 100644 --- a/i18n/keys.json +++ b/i18n/keys.json @@ -124,6 +124,14 @@ "defaultMessage": "Loading page", "description": "Default message to be announced by screen readers when the page is loading" }, + "page_header.default_loaded_message": { + "defaultMessage": "Page header loaded", + "description": "Default message to be announced by screen readers when the page header is loaded" + }, + "page_header.default_loading_message": { + "defaultMessage": "Loading page header", + "description": "Default message to be announced by screen readers when the page header is loading" + }, "pagination.next": { "defaultMessage": "Next", "description": "Label for the pagination button that takes you to the next page" diff --git a/src/components/layout/header/common/HeaderBase.tsx b/src/components/layout/header/common/HeaderBase.tsx index 9fdccbe1..e7955bf0 100644 --- a/src/components/layout/header/common/HeaderBase.tsx +++ b/src/components/layout/header/common/HeaderBase.tsx @@ -20,6 +20,10 @@ import styled from '@emotion/styled'; import { CSSProperties, forwardRef } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { LoadingStateProvider } from '~common/components/LoadingStateProvider'; +import { ScreenReaderOnlyLoadingStatus } from '~common/components/ScreenReaderOnlyLoadingStatus'; +import { isDefined } from '~common/helpers/types'; import { cssVar } from '~utils/design-tokens'; import { StyledDivider, @@ -46,6 +50,9 @@ const HeaderBase = forwardRef((prop description, hasDivider, hasFullWidthNav, + isLoading, + loadedMessage, + loadingMessage, metadata, navigation, title, @@ -53,33 +60,61 @@ const HeaderBase = forwardRef((prop } = props; return ( -
- {callout && {callout}} + <> +
+ + {callout && {callout}} - {breadcrumbs && {breadcrumbs}} + {breadcrumbs && {breadcrumbs}} - - {title} + + {title} - {metadata} + {metadata} - {description} - + {description} + - {actions && ( - {actions} - )} + {actions && ( + {actions} + )} - {navigation && ( - <> - {navigation} + {navigation && ( + <> + {navigation} - {hasDivider && } - - )} + {hasDivider && } + + )} - {hasDivider && !navigation && } -
+ {hasDivider && !navigation && } + +
+ + {isDefined(isLoading) && ( + + ) + } + loadingMessage={ + loadingMessage ?? ( + + ) + } + /> + )} + ); }); HeaderBase.displayName = 'HeaderBase'; diff --git a/src/components/layout/header/common/HeaderTypes.ts b/src/components/layout/header/common/HeaderTypes.ts index ddb25c11..2310c170 100644 --- a/src/components/layout/header/common/HeaderTypes.ts +++ b/src/components/layout/header/common/HeaderTypes.ts @@ -19,6 +19,7 @@ */ import { ReactNode } from 'react'; +import { TextNode } from '~types/utils'; export enum PageHeaderArea { breadcrumbs = 'breadcrumbs', @@ -109,6 +110,23 @@ export interface HeaderProps { * @defaultValue false */ hasDivider?: boolean; + /** + * Setting this prop will make this component behave like a LoadingContainer. + * It will provide a LoadingContext that LoadingSkeletons can consume (automatically), + * and deal with screen readers. + * Customize the accessible status messages by specifying `loadedMessage` and `loadingMessage`. + */ + isLoading?: boolean; + /** + * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * @defaultValue Page header loaded + */ + loadedMessage?: TextNode; + /** + * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * @defaultValue Loading page header + */ + loadingMessage?: TextNode; /** * Metadata elements to display below the title. Use to wrap them. */ From a4dd05a57fdf758737093d92b0372ce2a9d3a0ea Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Fri, 12 Jun 2026 09:42:33 +0200 Subject: [PATCH 4/6] Add tests to everything --- .../layout/__tests__/LayoutSlots-test.tsx | 53 ++++++++++++++++++- .../__tests__/ContentHeader-test.tsx | 32 +++++++++++ .../page-header/__tests__/PageHeader-test.tsx | 26 +++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/components/layout/__tests__/LayoutSlots-test.tsx b/src/components/layout/__tests__/LayoutSlots-test.tsx index 64787534..f69e3c5a 100644 --- a/src/components/layout/__tests__/LayoutSlots-test.tsx +++ b/src/components/layout/__tests__/LayoutSlots-test.tsx @@ -20,7 +20,7 @@ import { screen } from '@testing-library/react'; import { render } from '~common/helpers/test-utils'; -import { AsideLeft, PageGrid } from '../LayoutSlots'; +import { AsideLeft, PageContent, PageGrid } from '../LayoutSlots'; import { AsideSize, PageWidth } from '../LayoutTypes'; describe('PageGrid', () => { @@ -40,6 +40,57 @@ describe('PageGrid', () => { maxWidth: 'var(--echoes-layout-sizes-max-width-default)', }); }); + + it.each([ + ['loading', { isLoading: true }, 'true', 'Loading page'], + ['not loading', { isLoading: false }, 'false', 'Page loaded'], + [ + 'loading (custom message)', + { isLoading: true, loadingMessage: 'Fetching data' }, + 'true', + 'Fetching data', + ], + [ + 'not loading (custom message)', + { isLoading: false, loadedMessage: 'All done' }, + 'false', + 'All done', + ], + ])('should render correctly when %s', async (_, args, ariaBusy, expectedText) => { + const { container } = render(content); + + await expect(container).toHaveNoA11yViolations(); + + // eslint-disable-next-line testing-library/no-node-access + expect(container.firstChild).toHaveAttribute('aria-busy', ariaBusy); + expect(screen.getByText(expectedText)).toBeInTheDocument(); + }); +}); + +describe('PageContent', () => { + it.each([ + ['loading', { isLoading: true }, 'true', 'Loading page content'], + ['not loading', { isLoading: false }, 'false', 'Page content loaded'], + [ + 'loading (custom message)', + { isLoading: true, loadingMessage: 'Fetching data' }, + 'true', + 'Fetching data', + ], + [ + 'not loading (custom message)', + { isLoading: false, loadedMessage: 'All done' }, + 'false', + 'All done', + ], + ])('should render correctly when %s', async (_, args, ariaBusy, expectedText) => { + const { container } = render(content); + + await expect(container).toHaveNoA11yViolations(); + + expect(screen.getByRole('main')).toHaveAttribute('aria-busy', ariaBusy); + expect(screen.getByText(expectedText)).toBeInTheDocument(); + }); }); describe('AsideLeft', () => { diff --git a/src/components/layout/header/content-header/__tests__/ContentHeader-test.tsx b/src/components/layout/header/content-header/__tests__/ContentHeader-test.tsx index 68c82c44..16381145 100644 --- a/src/components/layout/header/content-header/__tests__/ContentHeader-test.tsx +++ b/src/components/layout/header/content-header/__tests__/ContentHeader-test.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { screen } from '@testing-library/react'; import { renderWithMemoryRouter } from '~common/helpers/test-utils'; import { ContentHeader } from '../..'; @@ -28,3 +29,34 @@ it('should display a ContentHeader properly', async () => { await expect(container).toHaveNoA11yViolations(); }); + +describe('isLoading', () => { + it.each([ + ['loading', { isLoading: true }, 'true', 'Loading page header'], + ['not loading', { isLoading: false }, 'false', 'Page header loaded'], + [ + 'loading (custom message)', + { isLoading: true, loadingMessage: 'Fetching data' }, + 'true', + 'Fetching data', + ], + [ + 'not loading (custom message)', + { isLoading: false, loadedMessage: 'All done' }, + 'false', + 'All done', + ], + ])('should render correctly when %s', async (_, args, ariaBusy, expectedText) => { + const { container } = renderWithMemoryRouter( + Awesome content header} + {...args} + />, + ); + + await expect(container).toHaveNoA11yViolations(); + + expect(screen.getByRole('banner')).toHaveAttribute('aria-busy', ariaBusy); + expect(screen.getByText(expectedText)).toBeInTheDocument(); + }); +}); diff --git a/src/components/layout/header/page-header/__tests__/PageHeader-test.tsx b/src/components/layout/header/page-header/__tests__/PageHeader-test.tsx index b7f47749..2ec84148 100644 --- a/src/components/layout/header/page-header/__tests__/PageHeader-test.tsx +++ b/src/components/layout/header/page-header/__tests__/PageHeader-test.tsx @@ -63,6 +63,32 @@ it('should display a minimal PageHeader properly', () => { expect(screen.getByText('Page title')).toBeInTheDocument(); }); +describe('isLoading', () => { + it.each([ + ['loading', { isLoading: true }, 'true', 'Loading page header'], + ['not loading', { isLoading: false }, 'false', 'Page header loaded'], + [ + 'loading (custom message)', + { isLoading: true, loadingMessage: 'Fetching data' }, + 'true', + 'Fetching data', + ], + [ + 'not loading (custom message)', + { isLoading: false, loadedMessage: 'All done' }, + 'false', + 'All done', + ], + ])('should render correctly when %s', async (_, args, ariaBusy, expectedText) => { + const { container } = setup(args); + + await expect(container).toHaveNoA11yViolations(); + + expect(screen.getByRole('banner')).toHaveAttribute('aria-busy', ariaBusy); + expect(screen.getByText(expectedText)).toBeInTheDocument(); + }); +}); + describe('scroll behavior', () => { it.each([ [PageHeaderScrollBehavior.collapse, false, 'sticky'], From 1159773a1f1224f275f9e116972fbe104134f729 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Fri, 12 Jun 2026 17:23:00 +0200 Subject: [PATCH 5/6] Add back HTML props (id is used in SQC) --- src/components/layout/LayoutSlots.tsx | 79 +++++++++++++++------------ 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/src/components/layout/LayoutSlots.tsx b/src/components/layout/LayoutSlots.tsx index 5bc40ab8..46816c61 100644 --- a/src/components/layout/LayoutSlots.tsx +++ b/src/components/layout/LayoutSlots.tsx @@ -19,7 +19,13 @@ */ import styled from '@emotion/styled'; -import { CSSProperties, forwardRef, PropsWithChildren } from 'react'; +import { + CSSProperties, + DetailedHTMLProps, + forwardRef, + HTMLAttributes, + PropsWithChildren, +} from 'react'; import { FormattedMessage } from 'react-intl'; import { LoadingStateProvider } from '~common/components/LoadingStateProvider'; import { ScreenReaderOnlyLoadingStatus } from '~common/components/ScreenReaderOnlyLoadingStatus'; @@ -213,43 +219,44 @@ export interface PageContentProps { loadingMessage?: TextNode; } -export const PageContent = forwardRef>( - (props, ref) => { - const { children, isLoading, loadedMessage, loadingMessage, ...restProps } = props; +export const PageContent = forwardRef< + HTMLElement, + PropsWithChildren, HTMLElement>> +>((props, ref) => { + const { children, isLoading, loadedMessage, loadingMessage, ...restProps } = props; - return ( - <> - - {children} - + return ( + <> + + {children} + - {isDefined(isLoading) && ( - - ) - } - loadingMessage={ - loadingMessage ?? ( - - ) - } - /> - )} - - ); - }, -); + {isDefined(isLoading) && ( + + ) + } + loadingMessage={ + loadingMessage ?? ( + + ) + } + /> + )} + + ); +}); PageContent.displayName = 'PageContent'; const StyledPageContent = styled.main` From 981b87a59175e5faaa8173df9ced385d1c151705 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Mon, 15 Jun 2026 18:06:36 +0200 Subject: [PATCH 6/6] Fixes from review --- src/components/layout/LayoutSlots.tsx | 20 ++++++++++--------- .../layout/header/common/HeaderTypes.ts | 4 ++-- .../LoadingSkeletonStyles.tsx | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/layout/LayoutSlots.tsx b/src/components/layout/LayoutSlots.tsx index 46816c61..185af866 100644 --- a/src/components/layout/LayoutSlots.tsx +++ b/src/components/layout/LayoutSlots.tsx @@ -111,12 +111,12 @@ export interface PageGridProps { */ isLoading?: boolean; /** - * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * Allows you to customize the screen reader-only status message announced when `isLoading` is false. * @defaultValue Page loaded */ loadedMessage?: TextNode; /** - * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * Allows you to customize the screen reader-only status message announced when `isLoading` is true. * @defaultValue Loading page */ loadingMessage?: TextNode; @@ -135,10 +135,12 @@ export const PageGrid = forwardRef - - {children} - + <> + + + {children} + + {isDefined(isLoading) && ( )} -
+ ); }, ); @@ -208,12 +210,12 @@ export interface PageContentProps { */ isLoading?: boolean; /** - * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * Allows you to customize the screen reader-only status message announced when `isLoading` is false. * @defaultValue Page content loaded */ loadedMessage?: TextNode; /** - * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * Allows you to customize the screen reader-only status message announced when `isLoading` is true. * @defaultValue Loading page content */ loadingMessage?: TextNode; diff --git a/src/components/layout/header/common/HeaderTypes.ts b/src/components/layout/header/common/HeaderTypes.ts index 2310c170..376efd9e 100644 --- a/src/components/layout/header/common/HeaderTypes.ts +++ b/src/components/layout/header/common/HeaderTypes.ts @@ -118,12 +118,12 @@ export interface HeaderProps { */ isLoading?: boolean; /** - * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * Allows you to customize the screen reader-only status message announced when `isLoading` is false. * @defaultValue Page header loaded */ loadedMessage?: TextNode; /** - * Allows you to customize the screen reader-only status message when `isLoading` is defined. + * Allows you to customize the screen reader-only status message announced when `isLoading` is true. * @defaultValue Loading page header */ loadingMessage?: TextNode; diff --git a/src/components/loading-skeleton/LoadingSkeletonStyles.tsx b/src/components/loading-skeleton/LoadingSkeletonStyles.tsx index 7e8d5cac..c345882d 100644 --- a/src/components/loading-skeleton/LoadingSkeletonStyles.tsx +++ b/src/components/loading-skeleton/LoadingSkeletonStyles.tsx @@ -34,7 +34,7 @@ const shimmer = keyframes` `; const LoadingSkeletonBaseStyle = styled.div` - background-color: ${cssVar('color-surface-hover')}; + background-color: ${cssVar('color-background-neutral-subtle-default')}; border-radius: ${cssVar('border-radius-200')};