Skip to content

Commit

Permalink
fix: Use recoveryText from i18n provider in Select (#1537)
Browse files Browse the repository at this point in the history
  • Loading branch information
Al-Dani authored Sep 26, 2023
1 parent 26cee13 commit 527c06e
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 20 deletions.
1 change: 1 addition & 0 deletions pages/autosuggest/recovery-test.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default function () {
enteredTextLabel={enteredTextLabel}
expandToViewport={true}
recoveryText="Try again"
onLoadItems={() => {}}
/>
</SpaceBetween>
</ScreenshotArea>
Expand Down
85 changes: 85 additions & 0 deletions src/autosuggest/__tests__/i18n.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<TestI18nProvider messages={{ autosuggest: { recoveryText: 'Custom recovery text' } }}>
<Autosuggest {...defaultProps} errorText="Error fetching items" statusType="error" onLoadItems={() => {}} />
</TestI18nProvider>
);
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(
<TestI18nProvider messages={{ autosuggest: { recoveryText: 'Custom recovery text' } }}>
<Autosuggest {...defaultProps} errorText="Error fetching items" statusType="error" />
</TestI18nProvider>
);
wrapper.focus();
expect(wrapper.findErrorRecoveryButton()).toBeNull();
});

test('supports providing errorIconAriaLabel', () => {
const { wrapper } = renderElement(
<TestI18nProvider messages={{ autosuggest: { errorIconAriaLabel: 'Custom error icon' } }}>
<Autosuggest {...defaultProps} errorText="Error fetching items" statusType="error" />
</TestI18nProvider>
);
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(
<TestI18nProvider messages={{ autosuggest: { enteredTextLabel: 'Custom use value' } }}>
<Autosuggest {...defaultProps} value="1" />
</TestI18nProvider>
);
wrapper.setInputValue('S');
expect(wrapper.findEnteredTextOption()!.getElement()).toHaveTextContent('Custom use value');
});

test('supports providing selectedAriaLabel', () => {
const { wrapper } = renderElement(
<TestI18nProvider messages={{ autosuggest: { selectedAriaLabel: 'Custom selected' } }}>
<Autosuggest {...defaultProps} value="1" />
</TestI18nProvider>
);
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');
});
});
5 changes: 3 additions & 2 deletions src/autosuggest/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r
options,
filteringType = 'auto',
statusType = 'finished',
recoveryText,
placeholder,
clearAriaLabel,
name,
Expand Down Expand Up @@ -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.');
Expand Down Expand Up @@ -179,6 +179,7 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r
errorIconAriaLabel,
onRecoveryClick: handleRecoveryClick,
filteringResultsText: filteredText,
hasRecoveryCallback: !!onLoadItems,
});

const shouldRenderDropdownContent = !isEmpty || dropdownStatus.content;
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
});
Expand Down
9 changes: 8 additions & 1 deletion src/internal/components/dropdown-status/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand All @@ -43,6 +48,7 @@ type UseDropdownStatus = ({
isNoMatch,
isFiltered,
noMatch,
hasRecoveryCallback,
onRecoveryClick,
}: DropdownStatusPropsExtended) => DropdownStatusResult;

Expand All @@ -64,6 +70,7 @@ export const useDropdownStatus: UseDropdownStatus = ({
isFiltered,
noMatch,
onRecoveryClick,
hasRecoveryCallback = false,
errorIconAriaLabel,
}) => {
const previousStatusType = usePrevious(statusType);
Expand All @@ -82,7 +89,7 @@ export const useDropdownStatus: UseDropdownStatus = ({
>
{errorText}
</InternalStatusIndicator>{' '}
{recoveryText && (
{!!recoveryText && hasRecoveryCallback && (
<InternalLink
onFollow={() => fireNonCancelableEvent(onRecoveryClick)}
variant="recovery"
Expand Down
85 changes: 85 additions & 0 deletions src/multiselect/__tests__/i18n.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<TestI18nProvider messages={{ multiselect: { deselectAriaLabel: 'Custom deselect {option__label}' } }}>
<Multiselect selectedOptions={[{ label: 'First', value: '1' }]} options={defaultOptions} />
</TestI18nProvider>
);
expect(wrapper.findToken(1)!.findDismiss().getElement()).toHaveAttribute('aria-label', 'Custom deselect First');
});

test('utilises recoveryText from Select messages', () => {
const { wrapper } = renderElement(
<TestI18nProvider messages={{ select: { recoveryText: 'Custom recovery text' } }}>
<Multiselect {...defaultProps} onLoadItems={() => {}} errorText="Error fetching items" statusType="error" />
</TestI18nProvider>
);
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(
<TestI18nProvider messages={{ select: { recoveryText: 'Custom recovery text' } }}>
<Multiselect {...defaultProps} errorText="Error fetching items" statusType="error" />
</TestI18nProvider>
);
wrapper.openDropdown();
expect(wrapper.findErrorRecoveryButton()).toBeNull();
});

test('utilises errorIconAriaLabel from Select messages', () => {
const { wrapper } = renderElement(
<TestI18nProvider messages={{ select: { errorIconAriaLabel: 'Custom error icon' } }}>
<Multiselect {...defaultProps} errorText="Error fetching items" statusType="error" />
</TestI18nProvider>
);

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(
<TestI18nProvider messages={{ select: { selectedAriaLabel: 'Custom selected' } }}>
<Multiselect {...defaultProps} selectedOptions={[{ label: 'First', value: '1' }]} />
</TestI18nProvider>
);

wrapper.openDropdown();
const selectedOption = wrapper.findDropdown()!.find('[data-test-index="1"]');
expect(selectedOption!.findByClassName(itemStyles['screenreader-content'])!.getElement()).toHaveTextContent(
'Custom selected First'
);
});
});
12 changes: 0 additions & 12 deletions src/multiselect/__tests__/multiselect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -498,14 +497,3 @@ test('Trigger receives focus when autofocus is true', () => {
const { wrapper } = renderMultiselect(<Multiselect selectedOptions={[]} options={groupOptions} autoFocus={true} />);
expect(document.activeElement).toBe(wrapper.findTrigger().getElement());
});

describe('i18n', () => {
test('supports providing deselectAriaLabel from i18n provider', () => {
const { wrapper } = renderMultiselect(
<TestI18nProvider messages={{ multiselect: { deselectAriaLabel: 'Custom deselect {option__label}' } }}>
<Multiselect selectedOptions={[{ label: 'First', value: '1' }]} options={defaultOptions} />
</TestI18nProvider>
);
expect(wrapper.findToken(1)!.findDismiss().getElement()).toHaveAttribute('aria-label', 'Custom deselect First');
});
});
10 changes: 7 additions & 3 deletions src/multiselect/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ const InternalMultiselect = React.forwardRef(
loadingText,
finishedText,
errorText,
recoveryText,
noMatch,
selectedAriaLabel,
renderHighlightedAriaLive,
selectedOptions = [],
deselectAriaLabel,
Expand All @@ -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,
Expand Down Expand Up @@ -199,7 +202,8 @@ const InternalMultiselect = React.forwardRef(
isFiltered,
filteringResultsText: filteredText,
onRecoveryClick: handleRecoveryClick,
errorIconAriaLabel: restProps.errorIconAriaLabel,
errorIconAriaLabel: errorIconAriaLabel,
hasRecoveryCallback: !!onLoadItems,
});

const filter = (
Expand Down
1 change: 1 addition & 0 deletions src/property-filter/property-filter-autosuggest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ const PropertyFilterAutosuggest = React.forwardRef(
...props,
isEmpty,
onRecoveryClick: handleRecoveryClick,
hasRecoveryCallback: !!onLoadItems,
});

let content = null;
Expand Down
Loading

0 comments on commit 527c06e

Please sign in to comment.