diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 311e5d332c..47af1f1888 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,8 +26,8 @@ jobs: - run: npm run build - run: npm run test:unit - integTest: - name: Components integ tests + integTestShards: + name: Components integ tests shard runs-on: ubuntu-latest strategy: matrix: @@ -42,8 +42,16 @@ jobs: - run: npm run build - run: npm run test:integ -- --shard=${{ matrix.shard }}/${{ strategy.job-total }} - a11yTest: - name: Components a11y tests + integTest: + name: Components integ tests + runs-on: ubuntu-latest + needs: + - integTestShards + steps: + - run: echo "Completed all integration tests" + + a11yTestShards: + name: Components a11y tests shard runs-on: ubuntu-latest strategy: matrix: @@ -58,6 +66,14 @@ jobs: - run: npm run build - run: npm run test:a11y -- --shard=${{ matrix.shard }}/${{ strategy.job-total }} + a11yTest: + name: Components a11y tests + runs-on: ubuntu-latest + needs: + - a11yTestShards + steps: + - run: echo "Completed all a11y tests" + release: uses: cloudscape-design/actions/.github/workflows/release.yml@main secrets: inherit diff --git a/package-lock.json b/package-lock.json index 4dde0bee91..c7c0ed704f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5827,9 +5827,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "engines": { "node": ">= 0.6" @@ -8160,9 +8160,9 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -8170,7 +8170,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", diff --git a/pages/app/components/page-view.scss b/pages/app/components/page-view.scss deleted file mode 100644 index fa0e599ce7..0000000000 --- a/pages/app/components/page-view.scss +++ /dev/null @@ -1,13 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ -@use '~design-tokens' as tokens; - -/* - This sets a background color to the page's container - to reveal the effects of negative z-index. -*/ -.page-container { - background-color: tokens.$color-background-container-content; -} diff --git a/pages/app/components/page-view.tsx b/pages/app/components/page-view.tsx index 25f9795b70..537f7d599e 100644 --- a/pages/app/components/page-view.tsx +++ b/pages/app/components/page-view.tsx @@ -5,8 +5,6 @@ import React, { lazy, Suspense } from 'react'; import pagesContext from '../pages-context'; import ErrorBoundary from './error-boundary'; -import styles from './page-view.scss'; - const pagesComponents: Record> = {}; export default function PageView({ pageId }: { pageId: string }) { @@ -17,9 +15,7 @@ export default function PageView({ pageId }: { pageId: string }) { return ( Loading...}> -
- -
+
); diff --git a/pages/area-chart/fit-height.page.tsx b/pages/area-chart/fit-height.page.tsx index 933ab917ee..8d1a3c0d57 100644 --- a/pages/area-chart/fit-height.page.tsx +++ b/pages/area-chart/fit-height.page.tsx @@ -31,7 +31,7 @@ export default function () { hide filter setUrlParams({ hideLegend: e.detail.checked })}> - hide legend + hide legend & y-title diff --git a/pages/common/flush-response.ts b/pages/common/flush-response.ts new file mode 100644 index 0000000000..697de7e450 --- /dev/null +++ b/pages/common/flush-response.ts @@ -0,0 +1,18 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export interface WindowWithFlushResponse extends Window { + __pendingCallbacks: Array<() => void>; + __flushServerResponse: () => void; +} +declare const window: WindowWithFlushResponse; + +export function enhanceWindow() { + window.__pendingCallbacks = []; + window.__flushServerResponse = () => { + for (const cb of window.__pendingCallbacks) { + cb(); + } + window.__pendingCallbacks = []; + }; +} diff --git a/pages/dropdown/width.page.tsx b/pages/dropdown/width.page.tsx index cfbc80b404..12b3c3ed88 100644 --- a/pages/dropdown/width.page.tsx +++ b/pages/dropdown/width.page.tsx @@ -9,6 +9,10 @@ import Select, { SelectProps } from '~components/select'; import SpaceBetween from '~components/space-between'; import AppContext, { AppContextType } from '../app/app-context'; +import { enhanceWindow, WindowWithFlushResponse } from '../common/flush-response'; + +declare const window: WindowWithFlushResponse; +enhanceWindow(); type DemoContext = React.Context< AppContextType<{ @@ -18,6 +22,7 @@ type DemoContext = React.Context< virtualScroll: boolean; expandToViewport: boolean; containerWidth: string; + manualServerMock: boolean; }> >; @@ -207,12 +212,17 @@ function CustomSelect({ expandToViewport, loading, onOpen, onClose, virtualScrol export default function () { const { urlParams } = useContext(AppContext as DemoContext); - const { asyncLoading, component, triggerWidth, virtualScroll, expandToViewport, containerWidth } = urlParams; + const { asyncLoading, component, triggerWidth, virtualScroll, expandToViewport, containerWidth, manualServerMock } = + urlParams; const [loading, setLoading] = useState(asyncLoading); const onOpen = () => { if (asyncLoading) { setLoading(true); - setTimeout(() => setLoading(false), 500); + if (manualServerMock) { + window.__pendingCallbacks.push(() => setLoading(false)); + } else { + setTimeout(() => setLoading(false), 500); + } } }; const onClose = () => setLoading(asyncLoading); diff --git a/pages/mixed-line-bar-chart/fit-height.page.tsx b/pages/mixed-line-bar-chart/fit-height.page.tsx index 0e25985b72..69e4447c36 100644 --- a/pages/mixed-line-bar-chart/fit-height.page.tsx +++ b/pages/mixed-line-bar-chart/fit-height.page.tsx @@ -30,7 +30,7 @@ export default function () { hide filter setUrlParams({ hideLegend: e.detail.checked })}> - hide legend + hide legend & y-title d.x)} yDomain={[0, 650]} xTitle="Food" - yTitle="Calories (kcal)" + yTitle={urlParams.hideLegend ? undefined : 'Calories (kcal)'} xScaleType="categorical" ariaLabel="Mixed chart 1" ariaDescription={barChartInstructions} diff --git a/pages/modal/with-component-load.page.tsx b/pages/modal/with-component-load.page.tsx deleted file mode 100644 index ee77118672..0000000000 --- a/pages/modal/with-component-load.page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React, { useEffect, useState } from 'react'; - -import { Box, Button, Checkbox, Modal, SpaceBetween, Spinner } from '~components'; -import { setPerformanceMetrics } from '~components/internal/analytics'; - -export default function () { - const [visible, setVisible] = useState(false); - const [buttonLoading, setButtonLoading] = useState(true); - const [textLoading, setTextLoading] = useState(true); - - useEffect(() => { - (window as any).modalPerformanceMetrics = []; - - setPerformanceMetrics({ - modalPerformanceData(props) { - (window as any).modalPerformanceMetrics.push(props); - }, - tableInteraction() {}, - taskCompletionData() {}, - }); - - return () => { - setPerformanceMetrics({ - tableInteraction: () => {}, - taskCompletionData: () => {}, - modalPerformanceData: () => {}, - }); - delete (window as any).modalPerformanceMetrics; - }; - }, []); - - const checkBoxesForLoadingStateChange = (id: string) => { - return ( - - - setButtonLoading(!buttonLoading)} - checked={buttonLoading} - > - Button Loading - - setTextLoading(!textLoading)} checked={textLoading}> - Text Loading - - - - ); - }; - return ( - -

Modal with loading component

- - {checkBoxesForLoadingStateChange('1')} - - - {'Header text'}} - visible={visible} - onDismiss={() => setVisible(false)} - closeAriaLabel="Close modal" - footer={ - - - - - } - > -
{textLoading ? : 'Content'}
- {checkBoxesForLoadingStateChange('2')} -
-
- ); -} diff --git a/pages/table/expandable-rows-test.page.tsx b/pages/table/expandable-rows-test.page.tsx index fbeea34450..99d3fed49c 100644 --- a/pages/table/expandable-rows-test.page.tsx +++ b/pages/table/expandable-rows-test.page.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useContext, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { isEqual } from 'lodash'; import { useCollection } from '@cloudscape-design/collection-hooks'; @@ -31,11 +31,15 @@ import messages from '~components/i18n/messages/all.en'; import SpaceBetween from '~components/space-between'; import AppContext, { AppContextType } from '../app/app-context'; +import { enhanceWindow, WindowWithFlushResponse } from '../common/flush-response'; import { ariaLabels, getHeaderCounterText, Instance } from './expandable-rows/common'; import { createColumns, createPreferences, filteringProperties } from './expandable-rows/expandable-rows-configs'; import { allInstances } from './expandable-rows/expandable-rows-data'; import { EmptyState, getMatchesCountText, renderAriaLive } from './shared-configs'; +declare const window: WindowWithFlushResponse; +enhanceWindow(); + type LoadingState = Map; type PageContext = React.Context< @@ -51,6 +55,7 @@ type PageContext = React.Context< useProgressiveLoading: boolean; useServerMock: boolean; emulateServerError: boolean; + manualServerMock: boolean; }> >; @@ -195,6 +200,18 @@ const NESTED_PAGE_SIZE = 2; function useTableData() { const settings = usePageSettings(); const delay = settings.useServerMock ? SERVER_DELAY : 0; + const getServerResponse = useCallback( + (cb: () => void) => { + if (settings.manualServerMock) { + window.__pendingCallbacks.push(cb); + return () => {}; + } else { + const timerRef = setTimeout(cb, delay); + return () => clearTimeout(timerRef); + } + }, + [delay, settings.manualServerMock] + ); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); @@ -204,13 +221,13 @@ function useTableData() { useEffect(() => { setLoading(true); setError(false); - setTimeout(() => { + return getServerResponse(() => { setReadyInstances(allInstances); setLoading(false); setError(settings.emulateServerError); - }, delay); + }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [delay, setLoading, setError, setReadyInstances]); + }, [getServerResponse, setLoading, setError, setReadyInstances]); const collectionResult = useCollection(readyInstances, { pagination: settings.usePagination ? { pageSize: ROOT_PAGE_SIZE } : undefined, @@ -245,14 +262,13 @@ function useTableData() { useEffect(() => { setLoading(true); setError(false); - const timeoutId = setTimeout(() => { + return getServerResponse(() => { setLoading(false); setReadyItems(memoItems); setError(settings.emulateServerError); - }, delay); - return () => clearTimeout(timeoutId); + }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [delay, memoItems, setLoading, setError, setReadyItems]); + }, [getServerResponse, memoItems, setLoading, setError, setReadyItems]); // Decorate path options to only show the last node and not the full path. collectionResult.propertyFilterProps.filteringOptions = collectionResult.propertyFilterProps.filteringOptions.map( @@ -277,7 +293,7 @@ function useTableData() { const loadItems = (id: string) => { setLoadingState(nextLoading(id)); if (delay) { - setTimeout(() => setLoadingState(settings.emulateServerError ? nextError(id) : nextPending(id)), delay); + getServerResponse(() => setLoadingState(settings.emulateServerError ? nextError(id) : nextPending(id))); } else { setLoadingState(nextPending(id)); } @@ -355,6 +371,7 @@ function usePageSettings() { useProgressiveLoading: urlParams.useProgressiveLoading ?? true, groupResources: urlParams.groupResources ?? true, useServerMock: urlParams.useServerMock ?? false, + manualServerMock: urlParams.manualServerMock ?? false, emulateServerError: urlParams.emulateServerError ?? false, setUrlParams, }; diff --git a/pages/table/simulated-server-actions.page.tsx b/pages/table/simulated-server-actions.page.tsx index 651a65b593..386c497db2 100644 --- a/pages/table/simulated-server-actions.page.tsx +++ b/pages/table/simulated-server-actions.page.tsx @@ -58,15 +58,10 @@ export default function TableLatencyMetricsPage() { console.log('tableInteraction:', props); }, taskCompletionData() {}, - modalPerformanceData() {}, }); return () => { - setPerformanceMetrics({ - tableInteraction: () => {}, - taskCompletionData: () => {}, - modalPerformanceData: () => {}, - }); + setPerformanceMetrics({ tableInteraction: () => {}, taskCompletionData: () => {} }); delete (window as any).tableInteractionMetrics; }; }, []); diff --git a/src/__integ__/page-objects/async-response-page.ts b/src/__integ__/page-objects/async-response-page.ts new file mode 100644 index 0000000000..d4c31b5429 --- /dev/null +++ b/src/__integ__/page-objects/async-response-page.ts @@ -0,0 +1,14 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; + +interface ExtendedWindow extends Window { + __flushServerResponse: () => void; +} +declare const window: ExtendedWindow; + +export class AsyncResponsePage extends BasePageObject { + flushResponse() { + return this.browser.execute(() => window.__flushServerResponse()); + } +} diff --git a/src/app-layout/__integ__/app-layout-focus-delegation.test.ts b/src/app-layout/__integ__/app-layout-focus-delegation.test.ts index 28cc93315a..e310d40404 100644 --- a/src/app-layout/__integ__/app-layout-focus-delegation.test.ts +++ b/src/app-layout/__integ__/app-layout-focus-delegation.test.ts @@ -5,13 +5,12 @@ import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; import createWrapper from '../../../lib/components/test-utils/selectors'; import { viewports } from './constants'; - -const testIf = (condition: boolean) => (condition ? test : test.skip); +import { getUrlParams, testIf, Theme } from './utils'; const wrapper = createWrapper().findAppLayout(); interface SetupTestObj { - theme: string; + theme: Theme; pageName: string; splitPanelPosition?: string; mobile: boolean; @@ -23,17 +22,16 @@ function setupTest( ) { return useBrowser(async browser => { const page = new BasePageObject(browser); - const params = new URLSearchParams({ - visualRefresh: `${theme.startsWith('refresh')}`, - appLayoutWidget: `${theme === 'refresh-toolbar'}`, - ...(splitPanelPosition + const params = getUrlParams( + theme, + splitPanelPosition ? { - splitPanelPosition, + splitPanelPosition: splitPanelPosition, } - : {}), - }); + : {} + ); await page.setWindowSize(mobile ? viewports.mobile : viewports.desktop); - await browser.url(`#/light/app-layout/${pageName}?${params.toString()}`); + await browser.url(`#/light/app-layout/${pageName}?${params}`); await page.waitForVisible(wrapper.findContentRegion().toSelector()); await testFn(page); }); @@ -53,19 +51,17 @@ describe.each(['classic', 'refresh', 'refresh-toolbar'] as const)('%s', theme => ); test( - 'split panel focus moves to slider on open and open button on close', + 'split panel focus moves to slider on open, and open button on close', setupTest( async page => { - const splitPanelOpenActionEl = - theme === 'refresh-toolbar' - ? wrapper.findDrawerTriggerById('slide-panel').toSelector() - : wrapper.findSplitPanel().findOpenButton().toSelector(); - await page.click(splitPanelOpenActionEl); + await page.click(wrapper.findSplitPanel().findOpenButton().toSelector()); await expect(page.isFocused(wrapper.findSplitPanel().findSlider().toSelector())).resolves.toBe(true); await page.keys(['Tab', 'Tab']); await expect(page.isFocused(wrapper.findSplitPanel().findCloseButton().toSelector())).resolves.toBe(true); await page.keys('Enter'); - await expect(page.isFocused(splitPanelOpenActionEl)).resolves.toBe(true); + await expect(page.isFocused(wrapper.findSplitPanel().findOpenButton().toSelector())).resolves.toBe(true); + await page.keys('Enter'); + await expect(page.isFocused(wrapper.findSplitPanel().findSlider().toSelector())).resolves.toBe(true); }, { pageName: 'with-split-panel', theme, mobile } ) @@ -87,6 +83,23 @@ describe.each(['classic', 'refresh', 'refresh-toolbar'] as const)('%s', theme => ) ); + test( + 'drawers focus toggles between open and close buttons', + setupTest( + async page => { + const triggerSelector = wrapper.findDrawerTriggerById('pro-help').toSelector(); + await page.click(triggerSelector); + await page.keys('Enter'); + await expect(page.isFocused(triggerSelector)).resolves.toBe(true); + await page.keys('Enter'); + await expect(page.isFocused(wrapper.findActiveDrawerCloseButton().toSelector())).resolves.toBe(true); + await page.keys('Enter'); + await expect(page.isFocused(triggerSelector)).resolves.toBe(true); + }, + { pageName: 'with-drawers', theme, mobile } + ) + ); + test( 'navigation panel focus toggles between open and close buttons', setupTest( @@ -107,37 +120,33 @@ describe.each(['classic', 'refresh', 'refresh-toolbar'] as const)('%s', theme => ) ); - //todo tools functionality needs to be added to toolbar - testIf(theme !== 'refresh-toolbar')( - 'focuses tools panel closed button when it is opened using keyboard and caused split panel to change position', + test( + 'focuses tools panel closed button when it is opened via keyboard and caused split panel to change position', setupTest( async page => { - await page.setWindowSize({ width: 1000, height: 800 }); + // Mobile nav is closed on page load + mobile && (await page.click(wrapper.findNavigationToggle().toSelector())); + await page.setWindowSize({ width: 1100, height: 800 }); await page.click(wrapper.findSplitPanel().findOpenButton().toSelector()); - await page.keys(['Tab', 'Tab', 'Tab', 'Tab', 'Enter']); + await expect(page.isExisting(wrapper.findSplitPanel().findOpenPanelSide().toSelector())).resolves.toBe( + true + ); + if (theme !== 'refresh-toolbar') { + await page.keys(['Tab', 'Tab', 'Tab', 'Tab', 'Enter']); + } else { + // Click the current page in breadcrumb to reset focus to toolbar + await page.click(wrapper.findBreadcrumbs().findBreadcrumbGroup().findBreadcrumbLink(2).toSelector()); + await page.keys(['Tab', 'Tab', 'Enter']); + } + await expect(page.isExisting(wrapper.findSplitPanel().findOpenPanelBottom().toSelector())).resolves.toBe( + true + ); await expect(page.isFocused(wrapper.findToolsClose().toSelector())).resolves.toBe(true); }, { pageName: 'with-split-panel', theme, mobile, splitPanelPosition: 'side' } ) ); - test( - 'focuses tools panel closed button when it is opened using keyboard and caused split panel to change position in toolbar theme', - setupTest( - async page => { - const triggerSelector = - theme === 'refresh-toolbar' - ? wrapper.findDrawerTriggerById('slide-panel').toSelector() - : wrapper.findSplitPanel().findOpenButton().toSelector(); - await page.setWindowSize({ width: 1000, height: 800 }); - await page.click(triggerSelector); - await page.keys(['Tab', 'Tab', 'Enter']); - await expect(page.isFocused(triggerSelector)).resolves.toBe(true); - }, - { pageName: 'with-split-panel', theme, mobile, splitPanelPosition: 'side' } - ) - ); - test( 'focuses split panel preferences button when its position changes from bottom to side', setupTest( @@ -241,47 +250,6 @@ describe.each(['classic', 'refresh', 'refresh-toolbar'] as const)('%s', theme => ); }); - test( - 'drawers focus toggles between open and close buttons', - setupTest( - async page => { - //Altermatomg between triggers because test-1 trigger hidden in overflow menu on mobile, - //security has resize button on desktop - const triggerSelector = wrapper.findDrawerTriggerById(mobile ? 'security' : 'test-1').toSelector(); - await page.click(triggerSelector); - await page.keys('Enter'); - await expect(page.isFocused(triggerSelector)).resolves.toBe(true); - await page.keys('Enter'); - await expect(page.isFocused(wrapper.findActiveDrawerCloseButton().toSelector())).resolves.toBe(true); - await page.keys('Enter'); - await expect(page.isFocused(triggerSelector)).resolves.toBe(true); - }, - { pageName: 'with-drawers', theme, mobile } - ) - ); - - test( - 'split panel focus toggles between open and close buttons', - setupTest( - async page => { - //Alternating between triggers because test-1 trigger hidden in overflow menu on mobile, - const triggerSelector = - theme === 'refresh-toolbar' - ? wrapper.findDrawerTriggerById('slide-panel').toSelector() - : wrapper.findSplitPanel().findOpenButton().toSelector(); - await page.click(triggerSelector); - await expect(page.isFocused(wrapper.findSplitPanel().findSlider().toSelector())).resolves.toBeTruthy(); - await page.keys(['Tab', 'Tab']); - await expect(page.isFocused(wrapper.findSplitPanel().findCloseButton().toSelector())).resolves.toBe(true); - await page.keys('Enter'); - await expect(page.isFocused(triggerSelector)).resolves.toBe(true); - await page.keys('Enter'); - await expect(page.isFocused(wrapper.findSplitPanel().findSlider().toSelector())).resolves.toBe(true); - }, - { pageName: 'with-drawers', theme, mobile } - ) - ); - describe('drawer focus interaction with tools buttons', () => { testIf(!mobile)( 'moves focus to close button when panel is opened from button', diff --git a/src/app-layout/__tests__/runtime-drawers.test.tsx b/src/app-layout/__tests__/runtime-drawers.test.tsx index c8d954e51f..7c30238f25 100644 --- a/src/app-layout/__tests__/runtime-drawers.test.tsx +++ b/src/app-layout/__tests__/runtime-drawers.test.tsx @@ -1042,5 +1042,111 @@ describe('toolbar mode only features', () => { expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('runtime drawer content'); }); + + describe('dynamically registered drawers with defaultActive: true', () => { + test('should open if there are already open local drawer on the page', async () => { + const { wrapper, globalDrawersWrapper } = await renderComponent(); + + wrapper.findDrawerTriggerById('security')!.click(); + expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('Security'); + + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global1', + type: 'global', + defaultActive: true, + }); + + await delay(); + + expect(globalDrawersWrapper.findDrawerById('global1')!.isActive()).toBe(true); + + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global2', + type: 'global', + defaultActive: true, + }); + + await delay(); + + expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('Security'); + expect(globalDrawersWrapper.findDrawerById('global1')!.isActive()).toBe(true); + expect(globalDrawersWrapper.findDrawerById('global2')!.isActive()).toBe(true); + }); + + test('should not open if there are already global drawers opened by user action on the page', async () => { + const { wrapper, globalDrawersWrapper } = await renderComponent(); + + wrapper.findDrawerTriggerById('security')!.click(); + expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('Security'); + + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global1', + type: 'global', + }); + + await delay(); + + wrapper.findDrawerTriggerById('global1')!.click(); + expect(globalDrawersWrapper.findDrawerById('global1')!.isActive()).toBe(true); + + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global2', + type: 'global', + defaultActive: true, + }); + + await delay(); + + expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('Security'); + expect(globalDrawersWrapper.findDrawerById('global1')!.isActive()).toBe(true); + expect(globalDrawersWrapper.findDrawerById('global2')).toBeFalsy(); + }); + + test('should not open if the maximum number (2) of global drawers is already open on the page', async () => { + const { wrapper, globalDrawersWrapper } = await renderComponent(); + + wrapper.findDrawerTriggerById('security')!.click(); + expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('Security'); + + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global1', + type: 'global', + defaultActive: true, + }); + + await delay(); + + expect(globalDrawersWrapper.findDrawerById('global1')!.isActive()).toBe(true); + + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global2', + type: 'global', + defaultActive: true, + }); + + await delay(); + + // this drawer should not open because there are already two global drawers open on the page, which is the maximum limit + awsuiPlugins.appLayout.registerDrawer({ + ...drawerDefaults, + id: 'global3', + type: 'global', + defaultActive: true, + }); + + await delay(); + + expect(wrapper.findActiveDrawer()!.getElement()).toHaveTextContent('Security'); + expect(globalDrawersWrapper.findDrawerById('global1')!.isActive()).toBe(true); + expect(globalDrawersWrapper.findDrawerById('global2')!.isActive()).toBe(true); + expect(globalDrawersWrapper.findDrawerById('global3')).toBeFalsy(); + }); + }); }); }); diff --git a/src/app-layout/constants.scss b/src/app-layout/constants.scss index 8cc5f990a5..65ce631e2b 100644 --- a/src/app-layout/constants.scss +++ b/src/app-layout/constants.scss @@ -29,6 +29,8 @@ $dashboard-content-widths: ( $drawer-z-index: 830; // should be above mobile toolbar $drawer-z-index-mobile: 1001; +// used in both mobile toolbar and vr-toolbar +$toolbar-z-index: 1000; // Shared toolbar drawer component values $toolbar-vertical-panel-icon-offset: 10px; diff --git a/src/app-layout/mobile-toolbar/styles.scss b/src/app-layout/mobile-toolbar/styles.scss index dad3cd1ca8..18b1f61c49 100644 --- a/src/app-layout/mobile-toolbar/styles.scss +++ b/src/app-layout/mobile-toolbar/styles.scss @@ -16,7 +16,7 @@ display: flex; align-items: center; flex-shrink: 0; - z-index: 1000; + z-index: constants.$toolbar-z-index; inline-size: 100%; box-sizing: border-box; background-color: awsui.$color-background-layout-mobile-panel; diff --git a/src/app-layout/utils/use-drawers.ts b/src/app-layout/utils/use-drawers.ts index fdce5fd12e..19bd196c00 100644 --- a/src/app-layout/utils/use-drawers.ts +++ b/src/app-layout/utils/use-drawers.ts @@ -63,8 +63,10 @@ function useRuntimeDrawers( const onLocalDrawerChangeStable = useStableCallback(onActiveDrawerChange); const onGlobalDrawersChangeStable = useStableCallback(onActiveGlobalDrawersChange); - const drawersWereOpenRef = useRef(false); - drawersWereOpenRef.current = drawersWereOpenRef.current || !!activeDrawerId || !!activeGlobalDrawersIds.length; + const localDrawerWasOpenRef = useRef(false); + localDrawerWasOpenRef.current = localDrawerWasOpenRef.current || !!activeDrawerId; + const activeGlobalDrawersIdsRef = useRef>([]); + activeGlobalDrawersIdsRef.current = activeGlobalDrawersIds; useEffect(() => { if (disableRuntimeDrawers) { @@ -74,17 +76,27 @@ function useRuntimeDrawers( const localDrawers = drawers.filter(drawer => drawer.type !== 'global'); const globalDrawers = drawers.filter(drawer => drawer.type === 'global'); setRuntimeDrawers(convertRuntimeDrawers(localDrawers, globalDrawers)); - if (!drawersWereOpenRef.current) { + if (!localDrawerWasOpenRef.current) { const defaultActiveLocalDrawer = sortByPriority(localDrawers).find(drawer => drawer.defaultActive); if (defaultActiveLocalDrawer) { onLocalDrawerChangeStable(defaultActiveLocalDrawer.id); } + } - const defaultActiveGlobalDrawers = sortByPriority(globalDrawers).filter(drawer => drawer.defaultActive); - defaultActiveGlobalDrawers.forEach(drawer => { - onGlobalDrawersChangeStable(drawer.id); - }); + const drawersNotActiveByDefault = globalDrawers.filter(drawer => !drawer.defaultActive); + const hasDrawersOpenByUserAction = drawersNotActiveByDefault.find(drawer => + activeGlobalDrawersIdsRef.current.includes(drawer.id) + ); + if (hasDrawersOpenByUserAction || activeGlobalDrawersIdsRef.current.length === DRAWERS_LIMIT) { + return; } + + const defaultActiveGlobalDrawers = sortByPriority(globalDrawers).filter( + drawer => !activeGlobalDrawersIdsRef.current.includes(drawer.id) && drawer.defaultActive + ); + defaultActiveGlobalDrawers.forEach(drawer => { + onGlobalDrawersChangeStable(drawer.id); + }); }); return () => { unsubscribe(); diff --git a/src/app-layout/visual-refresh-toolbar/index.tsx b/src/app-layout/visual-refresh-toolbar/index.tsx index b80d729ba1..6af2cccfa2 100644 --- a/src/app-layout/visual-refresh-toolbar/index.tsx +++ b/src/app-layout/visual-refresh-toolbar/index.tsx @@ -378,6 +378,7 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef diff --git a/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx b/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx index 1c8bf62ffb..df3251ae25 100644 --- a/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx +++ b/src/app-layout/visual-refresh-toolbar/toolbar/drawer-triggers.tsx @@ -142,7 +142,6 @@ export function DrawerTriggers({ selected={splitPanelToggleProps.active} ref={splitPanelFocusRef} hasTooltip={true} - testId={`awsui-app-layout-trigger-slide-panel`} isMobile={isMobile} isForSplitPanel={true} disabled={disabled} diff --git a/src/app-layout/visual-refresh-toolbar/toolbar/styles.scss b/src/app-layout/visual-refresh-toolbar/toolbar/styles.scss index 525dc282e4..4f58a8ca66 100644 --- a/src/app-layout/visual-refresh-toolbar/toolbar/styles.scss +++ b/src/app-layout/visual-refresh-toolbar/toolbar/styles.scss @@ -6,6 +6,7 @@ @use '../../../internal/styles' as styles; @use '../../../internal/styles/tokens' as awsui; @use '../../../internal/generated/custom-css-properties/index.scss' as custom-props; +@use '../../constants.scss' as constants; .universal-toolbar { background-color: awsui.$color-background-layout-panel-content; @@ -15,7 +16,7 @@ //right padding set in child trigger-container below for focus indicator to show correctly padding-inline-end: 0; position: sticky; - z-index: 840; + z-index: constants.$toolbar-z-index; @include styles.with-motion { transition: ease awsui.$motion-duration-refresh-only-slow; transition-property: inset-block-start, opacity; diff --git a/src/app-layout/visual-refresh/drawers.tsx b/src/app-layout/visual-refresh/drawers.tsx index e842a61bfc..3e880ec47d 100644 --- a/src/app-layout/visual-refresh/drawers.tsx +++ b/src/app-layout/visual-refresh/drawers.tsx @@ -300,7 +300,6 @@ function DesktopTriggers() { selected={hasSplitPanel && isSplitPanelOpen} ref={splitPanelRefs.toggle} highContrastHeader={headerVariant === 'high-contrast'} - testId="awsui-app-layout-trigger-slide-panel" /> )} diff --git a/src/app-layout/visual-refresh/mobile-toolbar.scss b/src/app-layout/visual-refresh/mobile-toolbar.scss index 7c12acde94..294dc94588 100644 --- a/src/app-layout/visual-refresh/mobile-toolbar.scss +++ b/src/app-layout/visual-refresh/mobile-toolbar.scss @@ -6,6 +6,7 @@ @use '../../internal/styles/' as styles; @use '../../internal/styles/tokens' as awsui; @use '../../internal/generated/custom-css-properties/index.scss' as custom-props; +@use '../constants.scss' as constants; section.mobile-toolbar { align-items: center; @@ -22,7 +23,7 @@ section.mobile-toolbar { padding-inline: awsui.$space-m; position: sticky; inset-block-start: var(#{custom-props.$offsetTop}); - z-index: 1000; + z-index: constants.$toolbar-z-index; &:not(.remove-high-contrast-header) { background-color: awsui.$color-background-layout-main; box-shadow: awsui.$shadow-panel-toggle; diff --git a/src/area-chart/chart-container.tsx b/src/area-chart/chart-container.tsx index 0ac0f56df7..74c887b090 100644 --- a/src/area-chart/chart-container.tsx +++ b/src/area-chart/chart-container.tsx @@ -44,6 +44,7 @@ interface ChartContainerProps model: ChartModel; autoWidth: (value: number) => void; fitHeight?: boolean; + hasFilters: boolean; minHeight: number; isRTL?: boolean; } @@ -71,6 +72,7 @@ function ChartContainer({ detailPopoverDismissAriaLabel, } = {}, fitHeight, + hasFilters, minHeight, xTickFormatter = deprecatedXTickFormatter, yTickFormatter = deprecatedYTickFormatter, @@ -119,6 +121,7 @@ function ChartContainer({ ref={mergedRef} minHeight={minHeight + blockEndLabelsProps.height} fitHeight={!!fitHeight} + hasFilters={hasFilters} leftAxisLabel={} leftAxisLabelMeasure={ ({ ariaDescription={ariaDescription} i18nStrings={i18nStrings} fitHeight={fitHeight} + hasFilters={!!showFilters} minHeight={height} isRTL={isRtl} /> diff --git a/src/button/internal.tsx b/src/button/internal.tsx index fd301287fc..c6aab3305f 100644 --- a/src/button/internal.tsx +++ b/src/button/internal.tsx @@ -25,7 +25,6 @@ import useForwardFocus from '../internal/hooks/forward-focus'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; import useHiddenDescription from '../internal/hooks/use-hidden-description'; import { useMergeRefs } from '../internal/hooks/use-merge-refs'; -import { useModalContextLoadingButtonComponent } from '../internal/hooks/use-modal-component-analytics'; import { usePerformanceMarks } from '../internal/hooks/use-performance-marks'; import { useUniqueId } from '../internal/hooks/use-unique-id'; import { checkSafeUrl } from '../internal/utils/check-safe-url'; @@ -122,7 +121,6 @@ export const InternalButton = React.forwardRef( }), [loading, disabled] ); - useModalContextLoadingButtonComponent(variant === 'primary', loading); const { targetProps, descriptionEl } = useHiddenDescription(disabledReason); diff --git a/src/collection-preferences/content-display/__tests__/content-display.test.tsx b/src/collection-preferences/content-display/__tests__/content-display.test.tsx index 75902de991..d934344498 100644 --- a/src/collection-preferences/content-display/__tests__/content-display.test.tsx +++ b/src/collection-preferences/content-display/__tests__/content-display.test.tsx @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import React from 'react'; import { fireEvent } from '@testing-library/react'; import { CollectionPreferencesProps } from '../../../../lib/components'; @@ -255,6 +256,8 @@ describe('Content Display preference', () => { contentDisplayPreference: { ...contentDisplayPreference, enableColumnFiltering: false, + // Adding an option with a non-string label to ensure the filter does not break rendering + options: [...contentDisplayPreference.options, { id: 'id-extra', label: (Extra) as any }], }, }); const filterInput = wrapper.findTextFilter(); diff --git a/src/collection-preferences/content-display/index.tsx b/src/collection-preferences/content-display/index.tsx index 254766fbe6..f4ee0da4bb 100644 --- a/src/collection-preferences/content-display/index.tsx +++ b/src/collection-preferences/content-display/index.tsx @@ -17,7 +17,7 @@ import ContentDisplayOption from './content-display-option'; import DraggableOption from './draggable-option'; import useDragAndDropReorder from './use-drag-and-drop-reorder'; import useLiveAnnouncements from './use-live-announcements'; -import { getSortedOptions, OptionWithVisibility } from './utils'; +import { getFilteredOptions, getSortedOptions, OptionWithVisibility } from './utils'; import styles from '../styles.css.js'; @@ -60,10 +60,7 @@ export default function ContentDisplayPreference({ const descriptionId = `${idPrefix}-description`; const sortedAndFilteredOptions = useMemo( - () => - getSortedOptions({ options, contentDisplay: value }).filter(option => - option.label.toLowerCase().trim().includes(columnFilteringText.toLowerCase().trim()) - ), + () => getFilteredOptions(getSortedOptions({ options, contentDisplay: value }), columnFilteringText), [columnFilteringText, options, value] ); diff --git a/src/collection-preferences/content-display/utils.ts b/src/collection-preferences/content-display/utils.ts index a6c4c62192..7939fe6cb2 100644 --- a/src/collection-preferences/content-display/utils.ts +++ b/src/collection-preferences/content-display/utils.ts @@ -24,3 +24,16 @@ export function getSortedOptions({ })) .filter(Boolean); } + +export function getFilteredOptions( + options: ReadonlyArray, + filterText: string +) { + filterText = filterText.trim().toLowerCase(); + + if (!filterText) { + return options; + } + + return options.filter(option => option.label.toLowerCase().trim().includes(filterText)); +} diff --git a/src/file-upload/__tests__/file-upload.test.tsx b/src/file-upload/__tests__/file-upload.test.tsx index f792f6ac41..7fea7a5de7 100644 --- a/src/file-upload/__tests__/file-upload.test.tsx +++ b/src/file-upload/__tests__/file-upload.test.tsx @@ -181,9 +181,24 @@ describe('FileUpload input', () => { test('file input fires onChange with files in details', () => { const wrapper = render({ multiple: true }); - fireEvent(wrapper.findNativeInput().getElement(), new CustomEvent('change', { bubbles: true })); + const input = wrapper.findNativeInput().getElement(); + Object.defineProperty(input, 'files', { value: [file1, file2] }); + fireEvent(input, new CustomEvent('change', { bubbles: true })); - expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { value: [] } })); + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { value: [file1, file2] } })); + // additional equality check, because `expect.objectContaining` above thinks file1 === file2 + expect((onChange as jest.Mock).mock.lastCall[0].detail.value[0]).toBe(file1); + expect((onChange as jest.Mock).mock.lastCall[0].detail.value[1]).toBe(file2); + }); + + test('file input fires onChange with only the first file if not multiple', () => { + const wrapper = render({}); + const input = wrapper.findNativeInput().getElement(); + Object.defineProperty(input, 'files', { value: [file1, file2] }); + fireEvent(input, new CustomEvent('change', { bubbles: true })); + + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ detail: { value: [file1] } })); + expect((onChange as jest.Mock).mock.lastCall[0].detail.value[0]).toBe(file1); }); }); diff --git a/src/file-upload/internal.tsx b/src/file-upload/internal.tsx index 6f2995b5f2..1470a0ca8b 100644 --- a/src/file-upload/internal.tsx +++ b/src/file-upload/internal.tsx @@ -85,7 +85,7 @@ function InternalFileUpload( } const handleFilesChange = (newFiles: File[]) => { - const newValue = multiple ? [...value, ...newFiles] : newFiles[0] ? newFiles : [...value]; + const newValue = multiple ? [...value, ...newFiles] : newFiles[0] ? newFiles.slice(0, 1) : [...value]; fireNonCancelableEvent(onChange, { value: newValue }); }; diff --git a/src/flashbar/styles.scss b/src/flashbar/styles.scss index cad0aeb351..638419326d 100644 --- a/src/flashbar/styles.scss +++ b/src/flashbar/styles.scss @@ -59,14 +59,6 @@ margin-block: 0; margin-inline: 0; - /* - Adds a new stacking context for the flashbar - This prevents the flashbar shadow to disappear behind its container's - background due to z-index: -1 - */ - position: relative; - z-index: 1; - &:not(.collapsed) { > li:not(:last-child) { margin-block-end: awsui.$space-xxxs; diff --git a/src/internal/analytics/__tests__/mocks.ts b/src/internal/analytics/__tests__/mocks.ts index 3a2b84a06a..fd307b6f02 100644 --- a/src/internal/analytics/__tests__/mocks.ts +++ b/src/internal/analytics/__tests__/mocks.ts @@ -26,11 +26,7 @@ export function mockFunnelMetrics() { } export function mockPerformanceMetrics() { - setPerformanceMetrics({ - tableInteraction: jest.fn(), - taskCompletionData: jest.fn(), - modalPerformanceData: jest.fn(), - }); + setPerformanceMetrics({ tableInteraction: jest.fn(), taskCompletionData: jest.fn() }); } export function mockInnerText() { diff --git a/src/internal/analytics/index.ts b/src/internal/analytics/index.ts index 716046cb2a..b2a4588403 100644 --- a/src/internal/analytics/index.ts +++ b/src/internal/analytics/index.ts @@ -47,7 +47,6 @@ export let FunnelMetrics: IFunnelMetrics = { export let PerformanceMetrics: IPerformanceMetrics = { tableInteraction(): void {}, taskCompletionData(): void {}, - modalPerformanceData(): void {}, }; export let ComponentMetrics: IComponentMetrics = { diff --git a/src/internal/analytics/interfaces.ts b/src/internal/analytics/interfaces.ts index 942ea0b369..9a069a671a 100644 --- a/src/internal/analytics/interfaces.ts +++ b/src/internal/analytics/interfaces.ts @@ -179,7 +179,6 @@ export type TaskCompletionDataMethod = (props: TaskCompletionDataProps) => void; export interface IPerformanceMetrics { tableInteraction: TableInteractionMethod; taskCompletionData: TaskCompletionDataMethod; - modalPerformanceData: ModalPerformanceDataMethod; } export interface ComponentMountedProps { @@ -191,20 +190,3 @@ export type ComponentMountedMethod = (props: ComponentMountedProps) => string; export interface IComponentMetrics { componentMounted: ComponentMountedMethod; } - -// Interface for modal metrics -export interface ModalPerformanceDataProps { - // Time span from when the modal begins loading to when the primary button or modal has finished loading. - // in milliseconds - timeToContentReadyInModal: number; - // Unique instance identifier for the component. - // Default: '' - instanceIdentifier?: string; - // Component identifier like modal header which can be used to identify the modal - // Default: '' - componentIdentifier?: string; - // Additional metadata related to modal - modalMetadata?: string; -} - -export type ModalPerformanceDataMethod = (props: ModalPerformanceDataProps) => void; diff --git a/src/internal/components/cartesian-chart/chart-container.tsx b/src/internal/components/cartesian-chart/chart-container.tsx index 2ec6451390..04588bb8a1 100644 --- a/src/internal/components/cartesian-chart/chart-container.tsx +++ b/src/internal/components/cartesian-chart/chart-container.tsx @@ -9,6 +9,7 @@ import styles from './styles.css.js'; interface CartesianChartContainerProps { minHeight: number; fitHeight: boolean; + hasFilters: boolean; leftAxisLabel: React.ReactNode; leftAxisLabelMeasure: React.ReactNode; bottomAxisLabel: React.ReactNode; @@ -28,6 +29,7 @@ export const CartesianChartContainer = forwardRef( bottomAxisLabel, chartPlot, popover, + hasFilters, }: CartesianChartContainerProps, ref: React.Ref ) => { @@ -36,7 +38,9 @@ export const CartesianChartContainer = forwardRef(
{leftAxisLabel} -
+
{leftAxisLabelMeasure}
diff --git a/src/internal/components/cartesian-chart/styles.scss b/src/internal/components/cartesian-chart/styles.scss index 614b4ff3a6..a08ddbc361 100644 --- a/src/internal/components/cartesian-chart/styles.scss +++ b/src/internal/components/cartesian-chart/styles.scss @@ -126,6 +126,9 @@ &.fit-height { flex: 1; + &:not(.axis-label + &, &.has-filters) { + margin-block-start: calc(0.5 * #{awsui.$font-chart-detail-size}); + } } } diff --git a/src/internal/components/dropdown/__integ__/width.test.ts b/src/internal/components/dropdown/__integ__/width.test.ts index 287c84daeb..cd05bcd4c8 100644 --- a/src/internal/components/dropdown/__integ__/width.test.ts +++ b/src/internal/components/dropdown/__integ__/width.test.ts @@ -1,13 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects'; import useBrowser from '@cloudscape-design/browser-test-tools/use-browser'; import createWrapper from '../../../../../lib/components/test-utils/selectors'; +import { AsyncResponsePage } from '../../../../__integ__/page-objects/async-response-page'; type ComponentId = 'autosuggest' | 'multiselect' | 'select'; -export class DropdownPageObject extends BasePageObject { +export class DropdownPageObject extends AsyncResponsePage { getWrapperAndTrigger(componentId: ComponentId) { const wrapper = createWrapper(); let componentWrapper; @@ -55,7 +55,7 @@ function setupTest( ) { return useBrowser({ width: pageWidth, height: 1000 }, async browser => { await browser.url( - `#/light/dropdown/width?component=${componentId}&expandToViewport=${expandToViewport}&triggerWidth=${triggerWidth}px&asyncLoading=${asyncLoading}` + `#/light/dropdown/width?component=${componentId}&expandToViewport=${expandToViewport}&triggerWidth=${triggerWidth}px&asyncLoading=${asyncLoading}&manualServerMock=${asyncLoading}` ); const page = new DropdownPageObject(browser); await page.waitForVisible(page.getWrapperAndTrigger(componentId).wrapper.toSelector()); @@ -148,9 +148,8 @@ describe('Dropdown width', () => { }); expect(dropdownBox.left + dropdownBox.width).toBeLessThanOrEqual(pageWidth); await expect(page.getText(dropdownSelector)).resolves.toContain('Loading'); - await page.waitUntil(async () => (await page.getText(dropdownSelector)).includes('A very'), { - timeout: 1000, - }); + await page.flushResponse(); + await expect(page.getText(dropdownSelector)).resolves.toContain('A very'); const newBox = await page.getBoundingBox(dropdownSelector); expect(newBox.left + newBox.width).toBeLessThanOrEqual(pageWidth); } diff --git a/src/internal/context/modal-context.ts b/src/internal/context/modal-context.ts index 5b65cfc778..9041b4f6ca 100644 --- a/src/internal/context/modal-context.ts +++ b/src/internal/context/modal-context.ts @@ -1,18 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { createContext, MutableRefObject, useContext } from 'react'; +import { createContext, useContext } from 'react'; -export interface ModalContextProps { - isInModal: boolean; - componentLoadingCount: MutableRefObject; - emitTimeToContentReadyInModal: (loadCompleteTime: number) => void; -} - -export const ModalContext = createContext({ - isInModal: false, - componentLoadingCount: { current: 0 }, - emitTimeToContentReadyInModal: () => {}, -}); +export const ModalContext = createContext<{ isInModal: boolean }>({ isInModal: false }); export const useModalContext = () => { const modalContext = useContext(ModalContext); diff --git a/src/internal/hooks/use-modal-component-analytics/__tests__/use-modal-component-analytics.test.tsx b/src/internal/hooks/use-modal-component-analytics/__tests__/use-modal-component-analytics.test.tsx deleted file mode 100644 index 1905dfb868..0000000000 --- a/src/internal/hooks/use-modal-component-analytics/__tests__/use-modal-component-analytics.test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React from 'react'; -import { render } from '@testing-library/react'; - -import * as useModalContext from '../../../context/modal-context'; -import { useModalContextLoadingButtonComponent, useModalContextLoadingComponent } from '../index'; -let mockModalContext: useModalContext.ModalContextProps, mockLoadTime: number; -beforeEach(() => { - mockLoadTime = 0; - jest.resetAllMocks(); - mockModalContext = { - isInModal: true, - componentLoadingCount: { current: 0 }, - emitTimeToContentReadyInModal: (loadTime: number) => { - mockLoadTime = loadTime; - }, - }; - jest.spyOn(useModalContext, 'useModalContext').mockReturnValue(mockModalContext); -}); - -describe('useModalContextLoadingButtonComponent', () => { - test('should set loadCompleteTime when element is loaded', () => { - const Demo = () => { - return ; - }; - render(); - expect(mockModalContext.componentLoadingCount.current).toBe(1); - - //wait for Demo to unmount - setTimeout(() => { - expect(mockModalContext.componentLoadingCount.current).toBe(0); - expect(mockLoadTime).not.toBe(0); - }, 100); - }); - - test('should not set loadCompleteTime if element is not inside modal', () => { - const Demo = () => { - return ; - }; - mockModalContext.isInModal = false; - render(); - expect(mockModalContext.componentLoadingCount.current).toBe(0); - expect(mockLoadTime).toBe(0); - }); - - test('should not set loadCompleteTime if componentLoadingCount is not 0 ', () => { - const Demo = () => { - return ; - }; - mockModalContext.componentLoadingCount.current = 2; - render(); - expect(mockModalContext.componentLoadingCount.current).toBe(3); - //wait for Demo to unmount - setTimeout(() => { - expect(mockModalContext.componentLoadingCount.current).toBe(2); - expect(mockLoadTime).toBe(0); - }, 100); - }); - - test('should not set componentLoadingCount or loadCompleteTime if element is not of type primary ', () => { - const Demo = () => { - return ; - }; - render(); - expect(mockModalContext.componentLoadingCount.current).toBe(0); - expect(mockLoadTime).toBe(0); - }); -}); - -describe('useModalContextLoadingComponent', () => { - test('should set loadCompleteTime when invoked inside a modal', () => { - const Demo = () => { - return
{useModalContextLoadingComponent()} content
; - }; - render(); - expect(mockModalContext.componentLoadingCount.current).toBe(1); - //wait for Demo to unmount - setTimeout(() => { - expect(mockModalContext.componentLoadingCount.current).toBe(0); - expect(mockLoadTime).not.toBe(0); - }, 100); - }); - - test('should not set componentLoadingCount or loadCompleteTime when invoked outside a modal', () => { - mockModalContext.isInModal = false; - const Demo = () => { - return
{useModalContextLoadingComponent()} content
; - }; - render(); - expect(mockModalContext.componentLoadingCount.current).toBe(0); - expect(mockLoadTime).toBe(0); - }); -}); diff --git a/src/internal/hooks/use-modal-component-analytics/index.ts b/src/internal/hooks/use-modal-component-analytics/index.ts deleted file mode 100644 index 2c75abe74e..0000000000 --- a/src/internal/hooks/use-modal-component-analytics/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { useEffect } from 'react'; - -import { useModalContext } from '../../context/modal-context'; - -export const useModalContextLoadingButtonComponent = (isPrimaryButton: boolean, loading: boolean) => { - const modalContext = useModalContext(); - useEffect(() => { - if (!isPrimaryButton || !modalContext.isInModal) { - return; - } - if (loading) { - modalContext.componentLoadingCount.current++; - return () => { - modalContext.componentLoadingCount.current--; - modalContext.emitTimeToContentReadyInModal(performance.now()); - }; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loading]); -}; - -export const useModalContextLoadingComponent = () => { - const modalContext = useModalContext(); - useEffect(() => { - if (!modalContext.isInModal) { - return; - } - modalContext.componentLoadingCount.current++; - return () => { - modalContext.componentLoadingCount.current--; - modalContext.emitTimeToContentReadyInModal(performance.now()); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); -}; diff --git a/src/internal/hooks/use-performance-marks/index.ts b/src/internal/hooks/use-performance-marks/index.ts index d3283e0a9f..342d0717a7 100644 --- a/src/internal/hooks/use-performance-marks/index.ts +++ b/src/internal/hooks/use-performance-marks/index.ts @@ -41,6 +41,7 @@ export function usePerformanceMarks( const id = useRandomId(); const { isInModal } = useModalContext(); const attributes = usePerformanceMarkAttribute(elementRef, id); + useEffect(() => { if (!enabled || !elementRef.current || isInModal) { return; diff --git a/src/internal/widgets/__tests__/widgets.test.tsx b/src/internal/widgets/__tests__/widgets.test.tsx index a179e3da7f..d3c58e0917 100644 --- a/src/internal/widgets/__tests__/widgets.test.tsx +++ b/src/internal/widgets/__tests__/widgets.test.tsx @@ -4,29 +4,13 @@ import React from 'react'; import { render } from '@testing-library/react'; import { useVisualRefresh } from '../../../../lib/components/internal/hooks/use-visual-mode'; -import { createWidgetizedComponent, createWidgetizedForwardRef } from '../../../../lib/components/internal/widgets'; +import { createWidgetizedComponent } from '../../../../lib/components/internal/widgets'; import { describeWithAppLayoutFeatureFlagEnabled } from './utils'; const LoaderSkeleton = () =>
Loading...
; const RealComponent = () =>
Real content
; const WidgetizedComponent = createWidgetizedComponent(RealComponent)(LoaderSkeleton); -const LoaderWithRef = React.forwardRef((props, ref) => ( -
- Loading... -
-)); -const RealComponentWithRef = React.forwardRef((props, ref) => ( -
- Real content -
-)); -const WidgetizedComponentWithRef = createWidgetizedForwardRef< - { children?: React.ReactNode }, - HTMLDivElement, - typeof RealComponentWithRef ->(RealComponentWithRef)(LoaderWithRef); - function findLoader(container: HTMLElement) { return container.querySelector('[data-testid="loader"]'); } @@ -77,23 +61,3 @@ describe('Refresh design', () => { }); }); }); - -describe('Ref handling', () => { - test('should forward ref to content', () => { - const ref = React.createRef(); - const { container } = render(); - expect(findContent(container)).toBeTruthy(); - expect(findLoader(container)).toBeFalsy(); - expect(ref.current).toHaveTextContent('Real content'); - }); - - describeWithAppLayoutFeatureFlagEnabled(() => { - test('should forward ref to loader', () => { - const ref = React.createRef(); - const { container } = render(); - expect(findContent(container)).toBeFalsy(); - expect(findLoader(container)).toBeTruthy(); - expect(ref.current).toHaveTextContent('Loading...'); - }); - }); -}); diff --git a/src/internal/widgets/index.tsx b/src/internal/widgets/index.tsx index 7464fe4aa7..d506b91120 100644 --- a/src/internal/widgets/index.tsx +++ b/src/internal/widgets/index.tsx @@ -21,20 +21,3 @@ export function createWidgetizedComponent>, ->(Implementation: Component) { - return (Loader?: Component): Component => { - return React.forwardRef((props, ref) => { - const isRefresh = useVisualRefresh(); - if (isRefresh && getGlobalFlag('appLayoutWidget') && Loader) { - return ; - } - - return ; - }) as Component; - }; -} diff --git a/src/mixed-line-bar-chart/chart-container.tsx b/src/mixed-line-bar-chart/chart-container.tsx index 27867edbef..bdc4eaa351 100644 --- a/src/mixed-line-bar-chart/chart-container.tsx +++ b/src/mixed-line-bar-chart/chart-container.tsx @@ -45,6 +45,7 @@ export interface ChartContainerProps { visibleSeries: ReadonlyArray>; fitHeight?: boolean; + hasFilters: boolean; height: number; detailPopoverSize: MixedLineBarChartProps['detailPopoverSize']; detailPopoverFooter: MixedLineBarChartProps['detailPopoverFooter']; @@ -103,6 +104,7 @@ const fallbackContainerWidth = 500; export default function ChartContainer({ fitHeight, + hasFilters, height: explicitPlotHeight, series, visibleSeries, @@ -506,6 +508,7 @@ export default function ChartContainer({ ref={containerRef} minHeight={explicitPlotHeight + blockEndLabelsProps.height} fitHeight={!!fitHeight} + hasFilters={hasFilters} leftAxisLabel={} leftAxisLabelMeasure={ { - const page = new BasePageObject(browser); - await browser.url('#/light/modal/with-component-load'); - const getModalPerformanceMetrics = () => - browser.execute(() => ((window as any).modalPerformanceMetrics ?? []) as ModalPerformanceDataProps[]); - await page.click('[data-testid="modal-trigger"]'); - let metrics = await getModalPerformanceMetrics(); - - //verify metrics are not emitted until all the components are loaded - expect(metrics.length).toBe(0); - - //set loading state to false - const wrapper = createWrapper(); - const buttonLoadingCheckBox = wrapper.findCheckbox('#checkbox-button2').findLabel().toSelector(); - const textLoadingCheckBox = wrapper.findCheckbox('#checkbox-text2').findLabel().toSelector(); - await page.click(buttonLoadingCheckBox); - await page.click(textLoadingCheckBox); - - metrics = await getModalPerformanceMetrics(); - expect(metrics[0].instanceIdentifier).not.toBeNull(); - expect(metrics[0].timeToContentReadyInModal).not.toBeNull(); - }) -); - -test( - 'should emit modal performance metrics with timeToContentReadyInModal 0 when components are already loaded', - useBrowser(async browser => { - const page = new BasePageObject(browser); - await browser.url('#/light/modal/with-component-load'); - const getModalPerformanceMetrics = () => - browser.execute(() => ((window as any).modalPerformanceMetrics ?? []) as ModalPerformanceDataProps[]); - - //load all components before opening the modal - const wrapper = createWrapper(); - const buttonLoadingCheckBox = wrapper.findCheckbox('#checkbox-button1').findLabel().toSelector(); - const textLoadingCheckBox = wrapper.findCheckbox('#checkbox-text1').findLabel().toSelector(); - await page.click(buttonLoadingCheckBox); - await page.click(textLoadingCheckBox); - - await page.click('[data-testid="modal-trigger"]'); - - // default interval after which modal metrics are automatically emitted. - const MODAL_READY_TIMEOUT = 100; - await delay(MODAL_READY_TIMEOUT); - - const metrics = await getModalPerformanceMetrics(); - expect(metrics[0].instanceIdentifier).not.toBeNull(); - expect(metrics[0].timeToContentReadyInModal).toBe(0); - }) -); - -test( - 'should not emit modal performance metrics more than once', - useBrowser(async browser => { - const page = new BasePageObject(browser); - await browser.url('#/light/modal/with-component-load'); - const getModalPerformanceMetrics = () => - browser.execute(() => ((window as any).modalPerformanceMetrics ?? []) as ModalPerformanceDataProps[]); - await page.click('[data-testid="modal-trigger"]'); - - //set loading state as false - const wrapper = createWrapper(); - const buttonLoadingCheckBox = wrapper.findCheckbox('#checkbox-button2').findLabel().toSelector(); - const textLoadingCheckBox = wrapper.findCheckbox('#checkbox-text2').findLabel().toSelector(); - await page.click(buttonLoadingCheckBox); - await page.click(textLoadingCheckBox); - - let metrics = await getModalPerformanceMetrics(); - expect(metrics[0].instanceIdentifier).not.toBeNull(); - expect(metrics[0].timeToContentReadyInModal).not.toBeNull(); - - //reload the components - await page.click(buttonLoadingCheckBox); - await page.click(textLoadingCheckBox); - - await page.click(buttonLoadingCheckBox); - await page.click(textLoadingCheckBox); - - metrics = await getModalPerformanceMetrics(); - expect(metrics.length).toBe(1); - }) -); - -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/modal/__tests__/modal.test.tsx b/src/modal/__tests__/modal.test.tsx index 94d43364e1..86d26f77db 100644 --- a/src/modal/__tests__/modal.test.tsx +++ b/src/modal/__tests__/modal.test.tsx @@ -4,7 +4,6 @@ import * as React from 'react'; import { act, fireEvent, render } from '@testing-library/react'; import Autosuggest from '../../../lib/components/autosuggest'; -import Button from '../../../lib/components/button/index.js'; import ButtonDropdown from '../../../lib/components/button-dropdown'; import DatePicker from '../../../lib/components/date-picker'; import DateRangePicker from '../../../lib/components/date-range-picker'; @@ -16,10 +15,10 @@ import Popover from '../../../lib/components/popover'; import Select from '../../../lib/components/select'; import StatusIndicator from '../../../lib/components/status-indicator'; import createWrapper, { ElementWrapper, ModalWrapper } from '../../../lib/components/test-utils/dom'; -import { PerformanceMetrics } from '../../internal/analytics'; import { KeyCode } from '../../internal/keycode'; import styles from '../../../lib/components/modal/styles.css.js'; + class ModalInternalWrapper extends ModalWrapper { findDialog(): ElementWrapper { return this.findByClassName(styles.dialog)!; @@ -530,15 +529,4 @@ describe('Modal component', () => { expect(wrapper.findContent().findInput()!.getElement()).not.toHaveAccessibleName('Outer label'); }); }); - describe('validates if modal performance metric is logged', () => { - it('validates if modal performance metric is logged', () => { - const modalPerformanceDataSpy = jest.spyOn(PerformanceMetrics, 'modalPerformanceData'); - const button =