diff --git a/pages/collection-preferences/reset-to-defaults.page.tsx b/pages/collection-preferences/reset-to-defaults.page.tsx new file mode 100644 index 0000000000..df1b58b770 --- /dev/null +++ b/pages/collection-preferences/reset-to-defaults.page.tsx @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import * as React from 'react'; + +import Box from '~components/box'; +import CollectionPreferences, { CollectionPreferencesProps } from '~components/collection-preferences'; + +import ScreenshotArea from '../utils/screenshot-area'; +import { contentDensityPreference, pageSizePreference, wrapLinesPreference } from './shared-configs'; + +const defaultPreferences: CollectionPreferencesProps.Preferences = { + pageSize: 10, + wrapLines: false, + contentDensity: 'comfortable', +}; + +const resetToDefaults: CollectionPreferencesProps.ResetToDefaults = { + label: 'Reset to defaults', + preferences: defaultPreferences, +}; + +export default function CollectionPreferencesResetToDefaults() { + const [preferences, setPreferences] = React.useState({ + pageSize: 50, + wrapLines: true, + contentDensity: 'compact', + }); + const [lastReset, setLastReset] = React.useState(null); + + return ( + <> +

CollectionPreferences with reset to defaults

+ + + Current preferences: {JSON.stringify(preferences)} + + + Last reset detail: {lastReset ? JSON.stringify(lastReset) : 'none'} + + setLastReset(detail)} + onConfirm={({ detail }) => setPreferences(detail)} + /> + + + ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index b54c1b56ab..6fb2d4f15c 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -9419,6 +9419,93 @@ The values for all configured preferences are present even if the user didn't ch "detailType": "CollectionPreferencesProps.Preferences", "name": "onConfirm", }, + { + "cancelable": false, + "description": "Called when the user restores the default preferences using the reset button in the modal footer. + +The event \`detail\` contains the default preferences the modal's working state was restored to +(the same object supplied in \`resetToDefaults.preferences\`).", + "detailInlineType": { + "name": "CollectionPreferencesProps.Preferences", + "properties": [ + { + "inlineType": { + "name": ""compact" | "comfortable"", + "type": "union", + "values": [ + "compact", + "comfortable", + ], + }, + "name": "contentDensity", + "optional": true, + "type": "string", + }, + { + "name": "contentDisplay", + "optional": true, + "type": "ReadonlyArray", + }, + { + "inlineType": { + "name": "NonNullable", + "type": "union", + "values": [ + "CustomPreferenceType", + "{}", + ], + }, + "name": "custom", + "optional": true, + "type": "NonNullable", + }, + { + "name": "pageSize", + "optional": true, + "type": "number", + }, + { + "inlineType": { + "name": "StickyColumns", + "properties": [ + { + "name": "first", + "optional": true, + "type": "number", + }, + { + "name": "last", + "optional": true, + "type": "number", + }, + ], + "type": "object", + }, + "name": "stickyColumns", + "optional": true, + "type": "StickyColumns", + }, + { + "name": "stripedRows", + "optional": true, + "type": "boolean", + }, + { + "name": "visibleContent", + "optional": true, + "type": "ReadonlyArray", + }, + { + "name": "wrapLines", + "optional": true, + "type": "boolean", + }, + ], + "type": "object", + }, + "detailType": "CollectionPreferencesProps.Preferences", + "name": "onReset", + }, ], "functions": [], "name": "CollectionPreferences", @@ -9925,6 +10012,116 @@ of the most recent getModalRoot call as an argument.", "optional": true, "type": "((container: HTMLElement | null) => void)", }, + { + "description": "Configures the built-in "reset to defaults" affordance. + +When set, the component displays a reset button in the modal footer that restores the modal's +working state to the provided default preferences in a single action. The user still needs to +confirm the change using the confirm button for it to take effect. + +It contains the following: +- \`label\` (string) - Specifies the text displayed on the reset button. +- \`preferences\` (CollectionPreferencesProps.Preferences) - Specifies the default preference values the modal + is restored to when the user activates the reset button.", + "i18nTag": true, + "inlineType": { + "name": "CollectionPreferencesProps.ResetToDefaults", + "properties": [ + { + "name": "label", + "optional": true, + "type": "string", + }, + { + "inlineType": { + "name": "CollectionPreferencesProps.Preferences", + "properties": [ + { + "inlineType": { + "name": ""compact" | "comfortable"", + "type": "union", + "values": [ + "compact", + "comfortable", + ], + }, + "name": "contentDensity", + "optional": true, + "type": "string", + }, + { + "name": "contentDisplay", + "optional": true, + "type": "ReadonlyArray", + }, + { + "inlineType": { + "name": "NonNullable", + "type": "union", + "values": [ + "CustomPreferenceType", + "{}", + ], + }, + "name": "custom", + "optional": true, + "type": "NonNullable", + }, + { + "name": "pageSize", + "optional": true, + "type": "number", + }, + { + "inlineType": { + "name": "StickyColumns", + "properties": [ + { + "name": "first", + "optional": true, + "type": "number", + }, + { + "name": "last", + "optional": true, + "type": "number", + }, + ], + "type": "object", + }, + "name": "stickyColumns", + "optional": true, + "type": "StickyColumns", + }, + { + "name": "stripedRows", + "optional": true, + "type": "boolean", + }, + { + "name": "visibleContent", + "optional": true, + "type": "ReadonlyArray", + }, + { + "name": "wrapLines", + "optional": true, + "type": "boolean", + }, + ], + "type": "object", + }, + "name": "preferences", + "optional": false, + "type": "CollectionPreferencesProps.Preferences", + }, + ], + "type": "object", + }, + "name": "resetToDefaults", + "optional": true, + "type": "CollectionPreferencesProps.ResetToDefaults", + }, { "description": "Configures the sticky columns preference that can be set for both left and right columns. @@ -38467,6 +38664,14 @@ To find a specific item use the \`findBreadcrumbLink(n)\` function as chaining \ "name": "PageSizePreferenceWrapper", }, }, + { + "name": "findResetButton", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ButtonWrapper", + }, + }, { "name": "findStickyColumnsPreference", "parameters": [ @@ -49646,6 +49851,14 @@ To find a specific item use the \`findBreadcrumbLink(n)\` function as chaining \ "name": "PageSizePreferenceWrapper", }, }, + { + "name": "findResetButton", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ButtonWrapper", + }, + }, { "name": "findStickyColumnsPreference", "parameters": [ 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 4b1af42bc5..632f778952 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 @@ -170,6 +170,7 @@ exports[`test-utils selectors 1`] = ` "awsui_page-size-form-field_tc96w", "awsui_page-size-radio-group_tc96w", "awsui_page-size_tc96w", + "awsui_reset-button_tc96w", "awsui_root_tc96w", "awsui_sticky-columns-first_tc96w", "awsui_sticky-columns-form-field_tc96w", diff --git a/src/collection-preferences/__tests__/collection-preferences.test.tsx b/src/collection-preferences/__tests__/collection-preferences.test.tsx index 0203bf8d90..d1d768abee 100644 --- a/src/collection-preferences/__tests__/collection-preferences.test.tsx +++ b/src/collection-preferences/__tests__/collection-preferences.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import CollectionPreferences from '../../../lib/components/collection-preferences'; +import CollectionPreferences, { CollectionPreferencesProps } from '../../../lib/components/collection-preferences'; import TestI18nProvider from '../../../lib/components/i18n/testing'; import createWrapper from '../../../lib/components/test-utils/dom'; import { CollectionPreferencesWrapper } from '../../../lib/components/test-utils/dom'; @@ -307,3 +307,74 @@ describe('i18n', () => { expect(modal.findContentDisplayPreference()!.find('[aria-label*="Custom drag handle"')).toBeTruthy(); }); }); + +describe('Collection preferences - Reset to defaults', () => { + const resetToDefaults: CollectionPreferencesProps.ResetToDefaults = { + label: 'Reset to defaults', + preferences: { pageSize: 10, wrapLines: false }, + }; + + test('does not render the reset button when resetToDefaults is not provided', () => { + const wrapper = renderCollectionPreferences({}); + wrapper.findTriggerButton().click(); + expect(wrapper.findModal()!.findResetButton()).toBeNull(); + }); + + test('renders the reset button with the provided label when resetToDefaults is set', () => { + const wrapper = renderCollectionPreferences({ resetToDefaults }); + wrapper.findTriggerButton().click(); + const resetButton = wrapper.findModal()!.findResetButton(); + expect(resetButton).not.toBeNull(); + expect(resetButton!.findTextRegion()!.getElement()).toHaveTextContent('Reset to defaults'); + }); + + test('calls onReset with the default preferences when clicking the reset button', () => { + const onResetSpy = jest.fn(); + const wrapper = renderCollectionPreferences({ + resetToDefaults, + onReset: onResetSpy, + preferences: { pageSize: 50, wrapLines: true }, + }); + wrapper.findTriggerButton().click(); + wrapper.findModal()!.findResetButton()!.click(); + expect(onResetSpy).toHaveBeenCalledTimes(1); + expect(onResetSpy).toHaveBeenCalledWith(expect.objectContaining({ detail: { pageSize: 10, wrapLines: false } })); + }); + + test('does not throw when onReset is not provided', () => { + const wrapper = renderCollectionPreferences({ resetToDefaults }); + wrapper.findTriggerButton().click(); + expect(() => wrapper.findModal()!.findResetButton()!.click()).not.toThrow(); + }); + + test('restores the working state so that confirm applies the default preferences', () => { + const onConfirmSpy = jest.fn(); + const wrapper = renderCollectionPreferences({ + resetToDefaults, + onConfirm: onConfirmSpy, + preferences: { pageSize: 50, wrapLines: true }, + }); + wrapper.findTriggerButton().click(); + wrapper.findModal()!.findResetButton()!.click(); + wrapper.findModal()!.findConfirmButton()!.click(); + expect(onConfirmSpy).toHaveBeenCalledWith(expect.objectContaining({ detail: { pageSize: 10, wrapLines: false } })); + }); + + test('does not close the modal when clicking the reset button', () => { + const wrapper = renderCollectionPreferences({ resetToDefaults }); + wrapper.findTriggerButton().click(); + wrapper.findModal()!.findResetButton()!.click(); + expect(wrapper.findModal()).not.toBeNull(); + }); + + test('supports using resetToDefaults label from i18n provider', () => { + const { container } = render( + + + + ); + const wrapper = createWrapper(container).findCollectionPreferences()!; + wrapper.findTriggerButton().click(); + expect(wrapper.findModal()!.findResetButton()!.findTextRegion()!.getElement()).toHaveTextContent('Custom reset'); + }); +}); diff --git a/src/collection-preferences/index.tsx b/src/collection-preferences/index.tsx index ec882f66c1..e8c1fae5c8 100644 --- a/src/collection-preferences/index.tsx +++ b/src/collection-preferences/index.tsx @@ -64,6 +64,8 @@ export default function CollectionPreferences({ getModalRoot, removeModalRoot, contentBefore, + resetToDefaults, + onReset, ...rest }: CollectionPreferencesProps) { const parentMetadata = useContext(CollectionPreferencesMetadata); @@ -110,6 +112,12 @@ export default function CollectionPreferences({ setTemporaryPreferences(copyPreferences(preferences || {})); }; + const onResetListener = () => { + const resetPreferences = copyPreferences(resetToDefaults?.preferences || {}); + setTemporaryPreferences(resetPreferences); + fireNonCancelableEvent(onReset, resetPreferences); + }; + const hasContentOnTheLeft = !!( pageSizePreference || wrapLinesPreference || @@ -188,28 +196,64 @@ export default function CollectionPreferences({ header={i18n('title', title)} referrerId={referrerId} footer={ - - + resetToDefaults ? ( +
- {i18n('cancelLabel', cancelLabel)} + {i18n('resetToDefaults.label', resetToDefaults.label)} - - {i18n('confirmLabel', confirmLabel)} - - - + + + + {i18n('cancelLabel', cancelLabel)} + + + {i18n('confirmLabel', confirmLabel)} + + + +
+ ) : ( + + + + {i18n('cancelLabel', cancelLabel)} + + + {i18n('confirmLabel', confirmLabel)} + + + + ) } closeAriaLabel={closeAriaLabel || cancelLabel} size={hasContentOnTheLeft && hasContentOnTheRight ? 'large' : 'medium'} diff --git a/src/collection-preferences/interfaces.ts b/src/collection-preferences/interfaces.ts index b6896879a1..1cf39a8e25 100644 --- a/src/collection-preferences/interfaces.ts +++ b/src/collection-preferences/interfaces.ts @@ -225,6 +225,27 @@ export interface CollectionPreferencesProps extends * Content displayed before the preferences. Use it to display additional information relating to the preferences. */ contentBefore?: React.ReactNode; + /** + * Configures the built-in "reset to defaults" affordance. + * + * When set, the component displays a reset button in the modal footer that restores the modal's + * working state to the provided default preferences in a single action. The user still needs to + * confirm the change using the confirm button for it to take effect. + * + * It contains the following: + * - `label` (string) - Specifies the text displayed on the reset button. + * - `preferences` (CollectionPreferencesProps.Preferences) - Specifies the default preference values the modal + * is restored to when the user activates the reset button. + * @i18n + */ + resetToDefaults?: CollectionPreferencesProps.ResetToDefaults; + /** + * Called when the user restores the default preferences using the reset button in the modal footer. + * + * The event `detail` contains the default preferences the modal's working state was restored to + * (the same object supplied in `resetToDefaults.preferences`). + */ + onReset?: NonCancelableEventHandler>; } export namespace CollectionPreferencesProps { @@ -239,6 +260,11 @@ export namespace CollectionPreferencesProps { custom?: CustomPreferenceType; } + export interface ResetToDefaults { + label?: string; + preferences: Preferences; + } + export interface ContentDisplayPreference extends DndAreaI18nStrings { title?: string; description?: string; diff --git a/src/collection-preferences/styles.scss b/src/collection-preferences/styles.scss index b938fc9db6..4bfce96ee7 100644 --- a/src/collection-preferences/styles.scss +++ b/src/collection-preferences/styles.scss @@ -13,11 +13,20 @@ .trigger-button, .cancel-button, .confirm-button, +.reset-button, .custom, .content-before { /* used in test-utils */ } +.footer { + display: flex; + align-items: center; + justify-content: space-between; + inline-size: 100%; + gap: awsui.$space-xs; +} + .second-column-small { padding-block-start: calc(2 * #{awsui.$space-scaled-l}); } diff --git a/src/i18n/messages-types.ts b/src/i18n/messages-types.ts index 63da31c470..88d438134b 100644 --- a/src/i18n/messages-types.ts +++ b/src/i18n/messages-types.ts @@ -118,6 +118,7 @@ export interface I18nFormatArgTypes { title: never; confirmLabel: never; cancelLabel: never; + 'resetToDefaults.label': never; 'pageSizePreference.title': never; 'wrapLinesPreference.label': never; 'wrapLinesPreference.description': never; diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index e075e74f0d..1b43f5cfb4 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -97,6 +97,7 @@ "title": "Preferences", "confirmLabel": "Confirm", "cancelLabel": "Cancel", + "resetToDefaults.label": "Reset to defaults", "pageSizePreference.title": "Page size", "wrapLinesPreference.label": "Wrap lines", "wrapLinesPreference.description": "Select to see all the text and wrap the lines", @@ -496,4 +497,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Loading next step", "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" } -} \ No newline at end of file +} diff --git a/src/test-utils/dom/collection-preferences/index.ts b/src/test-utils/dom/collection-preferences/index.ts index 3a69780118..b171dba5f7 100644 --- a/src/test-utils/dom/collection-preferences/index.ts +++ b/src/test-utils/dom/collection-preferences/index.ts @@ -23,6 +23,10 @@ class PreferencesModalWrapper extends ModalWrapper { return this.findComponent(`.${styles['confirm-button']}`, ButtonWrapper); } + findResetButton(): ButtonWrapper | null { + return this.findComponent(`.${styles['reset-button']}`, ButtonWrapper); + } + findWrapLinesPreference(): CheckboxWrapper | null { return this.findComponent(`.${styles['wrap-lines']}`, CheckboxWrapper); }