diff --git a/core/components/atoms/dropdown/Dropdown.tsx b/core/components/atoms/dropdown/Dropdown.tsx
index 57b560ef31..fb550be85e 100644
--- a/core/components/atoms/dropdown/Dropdown.tsx
+++ b/core/components/atoms/dropdown/Dropdown.tsx
@@ -21,6 +21,8 @@ type fetchOptionsFunction = (searchTerm: string) => Promise<{
options: OptionSchema[];
}>;
+export type ErrorType = 'DEFAULT' | 'NO_RECORDS_FOUND' | 'FAILED_TO_FETCH';
+
export type EventType =
| 'select-option'
| 'deselect-option'
@@ -228,6 +230,7 @@ interface DropdownState {
tempSelected: OptionSchema[];
previousSelected: OptionSchema[];
scrollIndex?: number;
+ errorType: ErrorType;
}
/**
@@ -243,6 +246,21 @@ interface DropdownState {
* * onChange: Called when selected options are updated.
* - Uncontrolled Dropdown:
* * onChange: Called when user `clicks on option` / `clicks on Clear, or Apply button`.
+ * 4. Default errorTemplate:
+ *
+ *
+ * (props) => {
+ * const { errorType = 'DEFAULT' } = props;
+ * const errorMessages = {
+ * 'FAILED\_TO\_FETCH': 'Failed to fetch data',
+ * 'NO\_RECORDS\_FOUND': 'No results found',
+ * 'DEFAULT': 'No results found'
+ * }
+ * return(
+ * \{errorMessages[errorType]}\
+ * );
+ * }
+ *
*/
export class Dropdown extends React.Component {
staticLimit: number;
@@ -282,6 +300,7 @@ export class Dropdown extends React.Component {
selected: _showSelectedItems(async, '', withCheckbox) ? selected : [],
triggerLabel: this.updateTriggerLabel(selectedGroup, optionsLength),
selectAll: getSelectAll(selectedGroup, optionsLength, disabledOptions.length),
+ errorType: 'DEFAULT',
};
}
@@ -384,7 +403,7 @@ export class Dropdown extends React.Component {
};
updateOptions = (init: boolean, async?: boolean) => {
- const { searchTerm, selectAll, tempSelected, previousSelected } = this.state;
+ const { searchTerm, selectAll, tempSelected, previousSelected, errorType } = this.state;
let updatedAsync = async === undefined ? this.state.async : async;
const { fetchOptions, withCheckbox, withSearch } = this.props;
@@ -409,6 +428,7 @@ export class Dropdown extends React.Component {
this.setState({
...this.state,
+ errorType: fetchOptions && errorType !== 'NO_RECORDS_FOUND' ? 'FAILED_TO_FETCH' : errorType,
scrollIndex: res.scrollToIndex || 0,
optionsLength,
loading: false,
@@ -433,6 +453,7 @@ export class Dropdown extends React.Component {
loading: true,
searchInit: true,
searchTerm: search,
+ errorType: 'NO_RECORDS_FOUND',
});
};
@@ -633,6 +654,17 @@ export class Dropdown extends React.Component {
);
});
+ reload = () => {
+ this.setState(
+ {
+ loading: true,
+ },
+ () => {
+ this.updateOptions(false);
+ }
+ );
+ };
+
debounceClear = debounce(250, () => this.updateOptions(false));
onClearOptions = () => {
@@ -757,6 +789,7 @@ export class Dropdown extends React.Component {
triggerLabel,
previousSelected,
scrollIndex,
+ errorType,
} = this.state;
const { withSelectAll = true, withCheckbox } = this.props;
@@ -796,6 +829,8 @@ export class Dropdown extends React.Component {
onSelectAll={this.onSelectAll}
customTrigger={triggerOptions.customTrigger}
scrollIndex={scrollIndex}
+ updateOptions={this.reload}
+ errorType={errorType}
{...rest}
/>
);
diff --git a/core/components/atoms/dropdown/DropdownList.tsx b/core/components/atoms/dropdown/DropdownList.tsx
index 4da1a92e01..2b46ee41fb 100644
--- a/core/components/atoms/dropdown/DropdownList.tsx
+++ b/core/components/atoms/dropdown/DropdownList.tsx
@@ -8,6 +8,8 @@ import classNames from 'classnames';
import Loading from './Loading';
import { BaseProps, extractBaseProps } from '@/utils/types';
import { ChangeEvent } from '@/common.type';
+import { ErrorTemplate } from './ErrorTemplate';
+import { ErrorType } from './Dropdown';
export type DropdownAlign = 'left' | 'right';
export type OptionType = 'DEFAULT' | 'WITH_ICON' | 'WITH_META' | 'ICON_WITH_META';
@@ -35,6 +37,10 @@ interface PopoverOptions {
type TriggerAndOptionProps = TriggerProps & OptionRendererProps;
+export interface ErrorTemplateProps {
+ errorType?: ErrorType;
+}
+
export interface DropdownListProps extends TriggerAndOptionProps {
/**
* Aligns the `Dropdown` left/right
@@ -43,9 +49,12 @@ export interface DropdownListProps extends TriggerAndOptionProps {
align?: DropdownAlign;
/**
* Display message when there is no result
- * @default "No result found"
*/
noResultMessage?: string;
+ /**
+ * Template to be rendered when **error: true**
+ */
+ errorTemplate?: React.FunctionComponent;
/**
* Label of Select All checkbox
* @default "Select All"
@@ -182,6 +191,9 @@ interface OptionsProps extends DropdownListProps, BaseProps {
onSearchChange?: (searchText: string) => void;
onOptionSelect: (selected: any[] | any) => void;
onSelect: (option: OptionSchema, checked: boolean) => void;
+ updateOptions: () => void;
+ errorType: ErrorType;
+ errorTemplate?: React.FunctionComponent;
}
export const usePrevious = (value: any) => {
@@ -224,6 +236,10 @@ const DropdownList = (props: OptionsProps) => {
className,
searchPlaceholder = 'Search..',
scrollIndex,
+ updateOptions,
+ noResultMessage,
+ errorType,
+ loadingOptions,
} = props;
const baseProps = extractBaseProps(props);
@@ -244,6 +260,8 @@ const DropdownList = (props: OptionsProps) => {
minHeight && setMinHeight(minHeight);
};
+ const isDropdownListBlank = listOptions.length === 0 && !loadingOptions && selected.length <= 0;
+
React.useEffect(() => {
let timer: any;
if (dropdownOpen) {
@@ -256,7 +274,6 @@ const DropdownList = (props: OptionsProps) => {
minWidth: minWidth ? minWidth : popperMinWidth,
maxWidth: maxWidth ? maxWidth : '100%',
};
-
requestAnimationFrame(getMinHeight);
setPopoverStyle(popperWrapperStyle);
@@ -325,6 +342,16 @@ const DropdownList = (props: OptionsProps) => {
minHeight: minHeight,
};
+ const defaultErrorTemplate = () => {
+ return (
+
+ );
+ };
+
const getDropdownSectionClass = (showClearButton?: boolean) => {
return classNames({
['Dropdown-section']: true,
@@ -550,7 +577,7 @@ const DropdownList = (props: OptionsProps) => {
selectedSectionLabel = 'Selected Items',
allItemsSectionLabel = 'All Items',
loadersCount = 10,
- loadingOptions,
+ errorTemplate = defaultErrorTemplate,
} = props;
const selectAllPresent = _isSelectAllPresent(searchTerm, remainingOptions, withSelectAll, withCheckbox);
@@ -566,15 +593,18 @@ const DropdownList = (props: OptionsProps) => {
);
}
- if (listOptions.length === 0 && !loadingOptions && selected.length <= 0) {
- const { noResultMessage = 'No result found' } = props;
- return (
-
-
-
{noResultMessage}
+ if (isDropdownListBlank) {
+ if (noResultMessage) {
+ return (
+
-
- );
+ );
+ } else {
+ return errorTemplate && errorTemplate({ errorType });
+ }
}
return (
@@ -689,6 +719,8 @@ const DropdownList = (props: OptionsProps) => {
}
};
+ const enableSearch = (withSearch || props.async) && (!isDropdownListBlank || errorType === 'NO_RECORDS_FOUND');
+
return (
//TODO(a11y)
//eslint-disable-next-line
@@ -703,7 +735,7 @@ const DropdownList = (props: OptionsProps) => {
{...popoverOptions}
data-test="DesignSystem-Dropdown--Popover"
>
- {(withSearch || props.async) && renderSearch()}
+ {enableSearch && renderSearch()}
{renderDropdownSection()}
{showApplyButton && withCheckbox && renderApplyButton()}
diff --git a/core/components/atoms/dropdown/ErrorTemplate.tsx b/core/components/atoms/dropdown/ErrorTemplate.tsx
new file mode 100644
index 0000000000..16e063f448
--- /dev/null
+++ b/core/components/atoms/dropdown/ErrorTemplate.tsx
@@ -0,0 +1,51 @@
+import * as React from 'react';
+import { Text, Button } from '@/index';
+import { ErrorType } from './Dropdown';
+
+interface ErrorTemplateProps {
+ dropdownStyle: React.CSSProperties;
+ errorType: ErrorType;
+ updateOptions: () => void;
+}
+
+const errorTitle: Record
= {
+ FAILED_TO_FETCH: 'Failed to fetch data',
+ NO_RECORDS_FOUND: 'No results found',
+ DEFAULT: 'No record available',
+};
+
+const errorDescription: Record = {
+ FAILED_TO_FETCH: "We couldn't load the data, try reloading.",
+ NO_RECORDS_FOUND: 'Try modifying your search to find what you are looking for.',
+ DEFAULT: 'We have nothing to show you at the moment.',
+};
+
+export const ErrorTemplate: React.FC = ({ dropdownStyle, errorType, updateOptions }) => {
+ return (
+
+
+
+ {errorTitle[errorType]}
+
+
+ {errorDescription[errorType]}
+
+ {errorType === 'FAILED_TO_FETCH' && (
+
+ )}
+
+
+ );
+};
diff --git a/core/components/atoms/dropdown/__stories__/Options.tsx b/core/components/atoms/dropdown/__stories__/Options.tsx
index 21d568ee05..9f855db5d7 100644
--- a/core/components/atoms/dropdown/__stories__/Options.tsx
+++ b/core/components/atoms/dropdown/__stories__/Options.tsx
@@ -224,3 +224,14 @@ export const fetchOptions = (searchTerm: string, options = dropdownOptions) => {
}, 1000);
});
};
+
+export const fetchEmptyOptions = () => {
+ return new Promise((resolve) => {
+ window.setTimeout(() => {
+ resolve({
+ options: [],
+ count: 0,
+ });
+ }, 1000);
+ });
+};
diff --git a/core/components/atoms/dropdown/__stories__/variants/WithErrorTemplate.story.jsx b/core/components/atoms/dropdown/__stories__/variants/WithErrorTemplate.story.jsx
new file mode 100644
index 0000000000..b65db29c36
--- /dev/null
+++ b/core/components/atoms/dropdown/__stories__/variants/WithErrorTemplate.story.jsx
@@ -0,0 +1,86 @@
+import * as React from 'react';
+import { Dropdown, Icon, Text } from '@/index';
+import { Uncontrolled, Controlled } from '../_common_/types';
+
+// CSF format story
+export const withErrorTemplate = () => {
+ const fetchOptions = (searchTerm) => {
+ return new Promise((resolve) => {
+ this.window.setTimeout(() => {
+ resolve({
+ searchTerm,
+ options: [],
+ count: 0,
+ });
+ }, 1000);
+ });
+ };
+
+ const errorTemplate = () => {
+ return (
+
+
+
+
+ Failed to fetch data
+
+
+ We couldn't load the data, try reloading.
+
+
+
+ );
+ };
+
+ return ;
+};
+
+const customCode = `() => {
+ const fetchOptions = (searchTerm) => {
+ return new Promise((resolve) => {
+ this.window.setTimeout(() => {
+ resolve({
+ searchTerm,
+ options: [],
+ count: 0,
+ });
+ }, 1000);
+ });
+ };
+
+ const errorTemplate = (errorType) => {
+ console.log(errorType);
+ return (
+
+
+
+
+ Failed to fetch data
+
+
+ We couldn't load the data, try reloading.
+
+
+
+ );
+ };
+
+ return ;
+}`;
+
+export default {
+ title: 'Inputs/Dropdown/Variants/With Error Template',
+ component: Dropdown,
+ parameters: {
+ docs: {
+ docPage: {
+ customCode,
+ title: 'Dropdown',
+ props: {
+ components: { Uncontrolled, Controlled },
+ exclude: ['showHead'],
+ },
+ },
+ },
+ },
+};
diff --git a/core/components/atoms/dropdown/__tests__/Dropdown.test.tsx b/core/components/atoms/dropdown/__tests__/Dropdown.test.tsx
index 46cb566c52..22f6fb05c5 100644
--- a/core/components/atoms/dropdown/__tests__/Dropdown.test.tsx
+++ b/core/components/atoms/dropdown/__tests__/Dropdown.test.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
import { testHelper, filterUndefined, valueHelper, testMessageHelper } from '@/utils/testHelper';
-import { Dropdown } from '@/index';
+import { Dropdown, Text } from '@/index';
import { DropdownProps as Props } from '@/index.type';
import { _isEqual } from '../utility';
import {
@@ -14,6 +14,7 @@ import {
fetchOptions,
preSelectedOptions,
allSelectedOptions,
+ fetchEmptyOptions,
} from '../__stories__/Options';
const size = ['tiny', 'regular'];
@@ -36,6 +37,8 @@ const trigger = 'DesignSystem-DropdownTrigger';
const keyDownEvents = ['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Default'];
+const errorTemplate = () => Test Error Message.;
+
describe('Dropdown component', () => {
const mapper: Record = {
triggerSize: valueHelper(size, { required: true, iterate: true }),
@@ -486,6 +489,29 @@ describe('Dropdown component with search', () => {
fireEvent.click(dropdownTrigger);
expect(screen.getByPlaceholderText('Custom search text')).toBeInTheDocument();
});
+
+ it('is not rendered when no record is available', () => {
+ const { getByTestId, queryByTestId } = render();
+ const dropdownTrigger = getByTestId(trigger);
+ fireEvent.click(dropdownTrigger);
+
+ expect(queryByTestId('DesignSystem-Input')).not.toBeInTheDocument();
+ });
+
+ it('is rendered when search returns no result', async () => {
+ const { getByTestId, getAllByTestId } = render();
+
+ const dropdownTrigger = getByTestId(trigger);
+ fireEvent.click(dropdownTrigger);
+
+ const searchInput = getByTestId('DesignSystem-Input');
+ fireEvent.change(searchInput, { target: { value: 'Option 101' } });
+
+ await waitFor(() => {
+ expect(getAllByTestId('DesignSystem-Dropdown--errorWrapper')).toHaveLength(1);
+ expect(searchInput).toBeInTheDocument();
+ });
+ });
});
describe('Dropdown component', () => {
@@ -729,3 +755,60 @@ describe('Dropdown component with all options selected', () => {
});
});
});
+
+describe('Dropdown errorTemplate', () => {
+ it('renders default template when no record is available', () => {
+ const { getByTestId, getAllByTestId } = render();
+
+ const dropdownTrigger = getByTestId(trigger);
+ fireEvent.click(dropdownTrigger);
+
+ expect(getAllByTestId('DesignSystem-Text')[0].textContent).toMatch('No record available');
+ expect(getAllByTestId('DesignSystem-Text')[1].textContent).toMatch('We have nothing to show you at the moment.');
+ });
+
+ it('renders default template when promise fetches no result', async () => {
+ const { getByTestId, getAllByTestId } = render();
+
+ const dropdownTrigger = getByTestId(trigger);
+ fireEvent.click(dropdownTrigger);
+
+ await waitFor(() => {
+ expect(getAllByTestId('DesignSystem-Text')[0].textContent).toMatch('Failed to fetch data');
+ expect(getAllByTestId('DesignSystem-Text')[1].textContent).toMatch("We couldn't load the data, try reloading.");
+ });
+ });
+
+ it('renders default template when search returns no result', async () => {
+ const { getByTestId, getAllByTestId } = render();
+
+ const dropdownTrigger = getByTestId(trigger);
+ fireEvent.click(dropdownTrigger);
+
+ const searchInput = getByTestId('DesignSystem-Input');
+ expect(searchInput).toBeInTheDocument();
+ fireEvent.change(searchInput, { target: { value: 'Option 101' } });
+
+ await waitFor(() => {
+ expect(getAllByTestId('DesignSystem-Text')[0].textContent).toMatch('No results found');
+ expect(getAllByTestId('DesignSystem-Text')[1].textContent).toMatch(
+ 'Try modifying your search to find what you are looking for.'
+ );
+ });
+ });
+
+ it('check: prop errorTemplate', async () => {
+ const { getByTestId } = render();
+
+ const dropdownTrigger = getByTestId(trigger);
+ fireEvent.click(dropdownTrigger);
+
+ const searchInput = getByTestId('DesignSystem-Input');
+ expect(searchInput).toBeInTheDocument();
+ fireEvent.change(searchInput, { target: { value: 'Option 101' } });
+
+ await waitFor(() => {
+ expect(screen.getByText('Test Error Message.')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/core/components/organisms/timePicker/__tests__/TimePickerWithSearch.test.tsx b/core/components/organisms/timePicker/__tests__/TimePickerWithSearch.test.tsx
index 41d3d0113a..d9459778a6 100644
--- a/core/components/organisms/timePicker/__tests__/TimePickerWithSearch.test.tsx
+++ b/core/components/organisms/timePicker/__tests__/TimePickerWithSearch.test.tsx
@@ -293,7 +293,7 @@ describe('TimePicker Event Handlers', () => {
describe('TimePicker Search Error Handlers', () => {
it('check for five digit search query in 12 hour format', async () => {
- const { getByTestId } = render();
+ const { getByTestId } = render();
const dropdownTrigger = getByTestId(trigger);
fireEvent.click(dropdownTrigger);
@@ -308,7 +308,9 @@ describe('TimePicker Search Error Handlers', () => {
});
it('check for invalid search', async () => {
- const { getByTestId } = render();
+ const { getByTestId } = render(
+
+ );
const dropdownTrigger = getByTestId(trigger);
fireEvent.click(dropdownTrigger);