From 527c06e53ea10a9e1f9a0923eaf9e47c4eb9deec Mon Sep 17 00:00:00 2001
From: Aleksandra Danilina <29692746+Al-Dani@users.noreply.github.com>
Date: Tue, 26 Sep 2023 13:04:50 +0200
Subject: [PATCH] fix: Use recoveryText from i18n provider in Select (#1537)
---
pages/autosuggest/recovery-test.page.tsx | 1 +
src/autosuggest/__tests__/i18n.test.tsx | 85 ++++++++++++++++++
src/autosuggest/internal.tsx | 5 +-
.../__tests__/dropdown-status.test.tsx | 16 +++-
.../components/dropdown-status/index.tsx | 9 +-
src/multiselect/__tests__/i18n.test.tsx | 85 ++++++++++++++++++
.../__tests__/multiselect.test.tsx | 12 ---
src/multiselect/internal.tsx | 10 ++-
.../property-filter-autosuggest.tsx | 1 +
src/select/__tests__/i18n.test.tsx | 87 +++++++++++++++++++
src/select/internal.tsx | 3 +-
11 files changed, 294 insertions(+), 20 deletions(-)
create mode 100644 src/autosuggest/__tests__/i18n.test.tsx
create mode 100644 src/multiselect/__tests__/i18n.test.tsx
create mode 100644 src/select/__tests__/i18n.test.tsx
diff --git a/pages/autosuggest/recovery-test.page.tsx b/pages/autosuggest/recovery-test.page.tsx
index da7973a95a..5cab1e81ce 100644
--- a/pages/autosuggest/recovery-test.page.tsx
+++ b/pages/autosuggest/recovery-test.page.tsx
@@ -29,6 +29,7 @@ export default function () {
enteredTextLabel={enteredTextLabel}
expandToViewport={true}
recoveryText="Try again"
+ onLoadItems={() => {}}
/>
diff --git a/src/autosuggest/__tests__/i18n.test.tsx b/src/autosuggest/__tests__/i18n.test.tsx
new file mode 100644
index 0000000000..14688abbed
--- /dev/null
+++ b/src/autosuggest/__tests__/i18n.test.tsx
@@ -0,0 +1,85 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import * as React from 'react';
+import { render } from '@testing-library/react';
+import createWrapper from '../../../lib/components/test-utils/dom';
+import Autosuggest, { AutosuggestProps } from '../../../lib/components/autosuggest';
+import '../../__a11y__/to-validate-a11y';
+import statusIconStyles from '../../../lib/components/status-indicator/styles.selectors.js';
+import TestI18nProvider from '../../../lib/components/i18n/testing';
+import itemStyles from '../../../lib/components/internal/components/selectable-item/styles.css.js';
+import { KeyCode } from '@cloudscape-design/test-utils-core/utils';
+
+const defaultOptions: AutosuggestProps.Options = [{ value: '1' }, { value: '2' }, { value: '3' }, { value: '4' }];
+const defaultProps: AutosuggestProps = {
+ value: '',
+ onChange: () => {},
+ options: defaultOptions,
+};
+
+function renderElement(jsx: React.ReactElement) {
+ const { container, rerender } = render(jsx);
+ const wrapper = createWrapper(container).findAutosuggest()!;
+ return { container, wrapper, rerender };
+}
+
+describe('i18n provider', () => {
+ test('supports providing recoveryText', () => {
+ const { wrapper } = renderElement(
+
+ {}} />
+
+ );
+ wrapper.focus();
+ expect(wrapper.findErrorRecoveryButton()!.getElement()).toHaveTextContent('Custom recovery text');
+ });
+
+ test('do not render recovery button if no recovery callback was provided', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+ wrapper.focus();
+ expect(wrapper.findErrorRecoveryButton()).toBeNull();
+ });
+
+ test('supports providing errorIconAriaLabel', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+ wrapper.focus();
+ const statusIcon = wrapper.findStatusIndicator()!.findByClassName(statusIconStyles.icon)!.getElement();
+ expect(statusIcon).toHaveAttribute('aria-label', 'Custom error icon');
+ });
+
+ test('supports providing enteredTextLabel', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+ wrapper.setInputValue('S');
+ expect(wrapper.findEnteredTextOption()!.getElement()).toHaveTextContent('Custom use value');
+ });
+
+ test('supports providing selectedAriaLabel', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+ wrapper.focus();
+ wrapper.findNativeInput().keydown(KeyCode.down);
+ wrapper.findNativeInput().keydown(KeyCode.down);
+ expect(
+ wrapper
+ .findDropdown()!
+ .find('[data-test-index="1"]')!
+ .findByClassName(itemStyles['screenreader-content'])!
+ .getElement()
+ ).toHaveTextContent('Custom selected');
+ });
+});
diff --git a/src/autosuggest/internal.tsx b/src/autosuggest/internal.tsx
index f268687e20..11ccdc7c79 100644
--- a/src/autosuggest/internal.tsx
+++ b/src/autosuggest/internal.tsx
@@ -44,7 +44,6 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r
options,
filteringType = 'auto',
statusType = 'finished',
- recoveryText,
placeholder,
clearAriaLabel,
name,
@@ -81,6 +80,7 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r
const i18n = useInternalI18n('autosuggest');
const errorIconAriaLabel = i18n('errorIconAriaLabel', restProps.errorIconAriaLabel);
const selectedAriaLabel = i18n('selectedAriaLabel', restProps.selectedAriaLabel);
+ const recoveryText = i18n('recoveryText', restProps.recoveryText);
if (!enteredTextLabel) {
warnOnce('Autosuggest', 'A value for enteredTextLabel must be provided.');
@@ -179,6 +179,7 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r
errorIconAriaLabel,
onRecoveryClick: handleRecoveryClick,
filteringResultsText: filteredText,
+ hasRecoveryCallback: !!onLoadItems,
});
const shouldRenderDropdownContent = !isEmpty || dropdownStatus.content;
@@ -241,7 +242,7 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r
/>
) : null
}
- loopFocus={statusType === 'error' && !!recoveryText}
+ loopFocus={statusType === 'error' && !!recoveryText && !!onLoadItems}
onCloseDropdown={handleCloseDropdown}
onDelayedInput={handleDelayedInput}
onPressArrowDown={handlePressArrowDown}
diff --git a/src/internal/components/dropdown-status/__tests__/dropdown-status.test.tsx b/src/internal/components/dropdown-status/__tests__/dropdown-status.test.tsx
index 9f7bb8b00f..3af7a3812c 100644
--- a/src/internal/components/dropdown-status/__tests__/dropdown-status.test.tsx
+++ b/src/internal/components/dropdown-status/__tests__/dropdown-status.test.tsx
@@ -38,13 +38,25 @@ describe('useDropdownStatus', () => {
expect(getContent()).toBe('hello world');
});
- test('renders error indicator', () => {
+ test('renders error text', () => {
const { getContent, getStickyState } = renderComponent({
statusType: 'error',
errorText: 'we got a problem',
recoveryText: 'do not worry',
});
+ expect(getStickyState()).toBe('true');
+ expect(getContent()).toBe('we got a problem');
+ });
+
+ test('renders error text with recovery button if has recovery callback', () => {
+ const { getContent, getStickyState } = renderComponent({
+ statusType: 'error',
+ errorText: 'we got a problem',
+ recoveryText: 'do not worry',
+ hasRecoveryCallback: true,
+ });
+
expect(getStickyState()).toBe('true');
expect(getContent()).toBe('we got a problem do not worry');
});
@@ -106,6 +118,7 @@ describe('useDropdownStatus', () => {
errorText: 'we got a problem',
recoveryText: 'do not worry',
errorIconAriaLabel: 'error-icon',
+ hasRecoveryCallback: true,
});
expect(getContent()).toBe('we got a problem do not worry');
@@ -139,6 +152,7 @@ describe('useDropdownStatus', () => {
isFiltered: true,
errorText: 'We got a problem',
recoveryText: 'do not worry',
+ hasRecoveryCallback: true,
});
expect(getContent()).toBe('We got a problem do not worry');
});
diff --git a/src/internal/components/dropdown-status/index.tsx b/src/internal/components/dropdown-status/index.tsx
index 491de55ac6..d152448514 100644
--- a/src/internal/components/dropdown-status/index.tsx
+++ b/src/internal/components/dropdown-status/index.tsx
@@ -25,6 +25,11 @@ export interface DropdownStatusPropsExtended extends DropdownStatusProps {
* to recover from the error.
*/
onRecoveryClick?: NonCancelableEventHandler;
+ /**
+ * Determines if retry button should be rendered
+ * in case recoveryText was automatically provided by i18n.
+ */
+ hasRecoveryCallback?: boolean;
}
function DropdownStatus({ children }: { children: React.ReactNode }) {
@@ -43,6 +48,7 @@ type UseDropdownStatus = ({
isNoMatch,
isFiltered,
noMatch,
+ hasRecoveryCallback,
onRecoveryClick,
}: DropdownStatusPropsExtended) => DropdownStatusResult;
@@ -64,6 +70,7 @@ export const useDropdownStatus: UseDropdownStatus = ({
isFiltered,
noMatch,
onRecoveryClick,
+ hasRecoveryCallback = false,
errorIconAriaLabel,
}) => {
const previousStatusType = usePrevious(statusType);
@@ -82,7 +89,7 @@ export const useDropdownStatus: UseDropdownStatus = ({
>
{errorText}
{' '}
- {recoveryText && (
+ {!!recoveryText && hasRecoveryCallback && (
fireNonCancelableEvent(onRecoveryClick)}
variant="recovery"
diff --git a/src/multiselect/__tests__/i18n.test.tsx b/src/multiselect/__tests__/i18n.test.tsx
new file mode 100644
index 0000000000..c6a4ae2755
--- /dev/null
+++ b/src/multiselect/__tests__/i18n.test.tsx
@@ -0,0 +1,85 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import * as React from 'react';
+import { render } from '@testing-library/react';
+import createWrapper from '../../../lib/components/test-utils/dom';
+import Multiselect, { MultiselectProps } from '../../../lib/components/multiselect';
+import '../../__a11y__/to-validate-a11y';
+import TestI18nProvider from '../../../lib/components/i18n/testing';
+import statusIconStyles from '../../../lib/components/status-indicator/styles.selectors.js';
+import itemStyles from '../../../lib/components/internal/components/selectable-item/styles.selectors.js';
+
+const defaultOptions: MultiselectProps.Options = [
+ { label: 'First', value: '1' },
+ { label: 'Second', value: '2' },
+ { label: 'Third', value: '3', lang: 'es' },
+ { label: 'Fourth', value: '4' },
+];
+
+const defaultProps = {
+ options: defaultOptions,
+ selectedOptions: [],
+ onChange: () => {},
+};
+
+function renderElement(jsx: React.ReactElement) {
+ const { container, rerender } = render(jsx);
+ const wrapper = createWrapper(container).findMultiselect()!;
+ return { container, wrapper, rerender };
+}
+describe('i18n provider', () => {
+ test('supports providing deselectAriaLabel', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+ expect(wrapper.findToken(1)!.findDismiss().getElement()).toHaveAttribute('aria-label', 'Custom deselect First');
+ });
+
+ test('utilises recoveryText from Select messages', () => {
+ const { wrapper } = renderElement(
+
+ {}} errorText="Error fetching items" statusType="error" />
+
+ );
+ wrapper.openDropdown();
+ expect(wrapper.findErrorRecoveryButton()!.getElement()).toHaveTextContent('Custom recovery text');
+ });
+
+ test('do not render recovery button if no recovery callback was provided', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+ wrapper.openDropdown();
+ expect(wrapper.findErrorRecoveryButton()).toBeNull();
+ });
+
+ test('utilises errorIconAriaLabel from Select messages', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+
+ wrapper.openDropdown();
+ const statusIcon = wrapper.findStatusIndicator()!.findByClassName(statusIconStyles.icon)!.getElement();
+ expect(statusIcon).toHaveAttribute('aria-label', 'Custom error icon');
+ });
+
+ test('utilises selectedAriaLabel from Select messages', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+
+ wrapper.openDropdown();
+ const selectedOption = wrapper.findDropdown()!.find('[data-test-index="1"]');
+ expect(selectedOption!.findByClassName(itemStyles['screenreader-content'])!.getElement()).toHaveTextContent(
+ 'Custom selected First'
+ );
+ });
+});
diff --git a/src/multiselect/__tests__/multiselect.test.tsx b/src/multiselect/__tests__/multiselect.test.tsx
index b1250268e0..4aac38878c 100644
--- a/src/multiselect/__tests__/multiselect.test.tsx
+++ b/src/multiselect/__tests__/multiselect.test.tsx
@@ -8,7 +8,6 @@ import tokenGroupStyles from '../../../lib/components/token-group/styles.css.js'
import selectPartsStyles from '../../../lib/components/select/parts/styles.css.js';
import '../../__a11y__/to-validate-a11y';
import statusIconStyles from '../../../lib/components/status-indicator/styles.selectors.js';
-import TestI18nProvider from '../../../lib/components/i18n/testing';
const defaultOptions: MultiselectProps.Options = [
{ label: 'First', value: '1' },
@@ -498,14 +497,3 @@ test('Trigger receives focus when autofocus is true', () => {
const { wrapper } = renderMultiselect();
expect(document.activeElement).toBe(wrapper.findTrigger().getElement());
});
-
-describe('i18n', () => {
- test('supports providing deselectAriaLabel from i18n provider', () => {
- const { wrapper } = renderMultiselect(
-
-
-
- );
- expect(wrapper.findToken(1)!.findDismiss().getElement()).toHaveAttribute('aria-label', 'Custom deselect First');
- });
-});
diff --git a/src/multiselect/internal.tsx b/src/multiselect/internal.tsx
index f709231551..e5522cb8c6 100644
--- a/src/multiselect/internal.tsx
+++ b/src/multiselect/internal.tsx
@@ -57,9 +57,7 @@ const InternalMultiselect = React.forwardRef(
loadingText,
finishedText,
errorText,
- recoveryText,
noMatch,
- selectedAriaLabel,
renderHighlightedAriaLive,
selectedOptions = [],
deselectAriaLabel,
@@ -85,6 +83,11 @@ const InternalMultiselect = React.forwardRef(
const formFieldContext = useFormFieldContext(restProps);
const i18n = useInternalI18n('multiselect');
+ const i18nCommon = useInternalI18n('select');
+ const recoveryText = i18nCommon('recoveryText', restProps.recoveryText);
+ const errorIconAriaLabel = i18nCommon('errorIconAriaLabel', restProps.errorIconAriaLabel);
+ const selectedAriaLabel = i18nCommon('selectedAriaLabel', restProps.selectedAriaLabel);
+
const { handleLoadMore, handleRecoveryClick, fireLoadItems } = useLoadItems({
onLoadItems,
options,
@@ -199,7 +202,8 @@ const InternalMultiselect = React.forwardRef(
isFiltered,
filteringResultsText: filteredText,
onRecoveryClick: handleRecoveryClick,
- errorIconAriaLabel: restProps.errorIconAriaLabel,
+ errorIconAriaLabel: errorIconAriaLabel,
+ hasRecoveryCallback: !!onLoadItems,
});
const filter = (
diff --git a/src/property-filter/property-filter-autosuggest.tsx b/src/property-filter/property-filter-autosuggest.tsx
index c4040cdc06..3ab84fa067 100644
--- a/src/property-filter/property-filter-autosuggest.tsx
+++ b/src/property-filter/property-filter-autosuggest.tsx
@@ -157,6 +157,7 @@ const PropertyFilterAutosuggest = React.forwardRef(
...props,
isEmpty,
onRecoveryClick: handleRecoveryClick,
+ hasRecoveryCallback: !!onLoadItems,
});
let content = null;
diff --git a/src/select/__tests__/i18n.test.tsx b/src/select/__tests__/i18n.test.tsx
new file mode 100644
index 0000000000..1526d6d27b
--- /dev/null
+++ b/src/select/__tests__/i18n.test.tsx
@@ -0,0 +1,87 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import * as React from 'react';
+import { render } from '@testing-library/react';
+import createWrapper from '../../../lib/components/test-utils/dom';
+import Select, { SelectProps } from '../../../lib/components/select';
+import '../../__a11y__/to-validate-a11y';
+import statusIconStyles from '../../../lib/components/status-indicator/styles.selectors.js';
+import TestI18nProvider from '../../../lib/components/i18n/testing';
+import itemStyles from '../../../lib/components/internal/components/selectable-item/styles.selectors.js';
+
+const defaultOptions: SelectProps.Options = [
+ { label: 'First', value: '1' },
+ { label: 'Second', value: '2' },
+ {
+ label: 'Group',
+ options: [
+ {
+ label: 'Third',
+ value: '3',
+ lang: 'de',
+ },
+ {
+ label: 'Forth',
+ value: '4',
+ },
+ ],
+ },
+];
+
+const defaultProps = {
+ options: defaultOptions,
+ selectedOption: null,
+ onChange: () => {},
+};
+
+function renderElement(jsx: React.ReactElement) {
+ const { container, rerender } = render(jsx);
+ const wrapper = createWrapper(container).findSelect()!;
+ return { container, wrapper, rerender };
+}
+
+describe('i18n provider', () => {
+ test('supports providing recoveryText', () => {
+ const { wrapper } = renderElement(
+
+
+ );
+ wrapper.openDropdown();
+ expect(wrapper.findErrorRecoveryButton()!.getElement()).toHaveTextContent('Custom recovery text');
+ });
+
+ test('do not render recovery button if no recovery callback was provided', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+ wrapper.openDropdown();
+ expect(wrapper.findErrorRecoveryButton()).toBeNull();
+ });
+
+ test('supports providing errorIconAriaLabel', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+ wrapper.openDropdown();
+ const statusIcon = wrapper.findStatusIndicator()!.findByClassName(statusIconStyles.icon)!.getElement();
+ expect(statusIcon).toHaveAttribute('aria-label', 'Custom error icon');
+ });
+
+ test('supports providing selectedAriaLabel', () => {
+ const { wrapper } = renderElement(
+
+
+
+ );
+ wrapper.openDropdown();
+ const selectedOption = wrapper.findDropdown()!.find('[data-test-index="1"]');
+ expect(selectedOption!.findByClassName(itemStyles['screenreader-content'])!.getElement()).toHaveTextContent(
+ 'Custom selected'
+ );
+ });
+});
diff --git a/src/select/internal.tsx b/src/select/internal.tsx
index 3adbd26d61..83b2485222 100644
--- a/src/select/internal.tsx
+++ b/src/select/internal.tsx
@@ -54,7 +54,6 @@ const InternalSelect = React.forwardRef(
loadingText,
finishedText,
errorText,
- recoveryText,
noMatch,
triggerVariant = 'label',
renderHighlightedAriaLive,
@@ -78,6 +77,7 @@ const InternalSelect = React.forwardRef(
const i18n = useInternalI18n('select');
const errorIconAriaLabel = i18n('errorIconAriaLabel', restProps.errorIconAriaLabel);
const selectedAriaLabel = i18n('selectedAriaLabel', restProps.selectedAriaLabel);
+ const recoveryText = i18n('recoveryText', restProps.recoveryText);
const { handleLoadMore, handleRecoveryClick, fireLoadItems } = useLoadItems({
onLoadItems,
@@ -192,6 +192,7 @@ const InternalSelect = React.forwardRef(
filteringResultsText: filteredText,
errorIconAriaLabel,
onRecoveryClick: handleRecoveryClick,
+ hasRecoveryCallback: !!onLoadItems,
});
const menuProps = {