Skip to content

Commit

Permalink
feat(dropdown): add error and empty state template in dropdown component
Browse files Browse the repository at this point in the history
  • Loading branch information
Satyam Chatterjee authored and anuradha9712 committed Sep 27, 2023
1 parent 0b637f9 commit 3f84aa6
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 16 deletions.
37 changes: 36 additions & 1 deletion core/components/atoms/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -228,6 +230,7 @@ interface DropdownState {
tempSelected: OptionSchema[];
previousSelected: OptionSchema[];
scrollIndex?: number;
errorType: ErrorType;
}

/**
Expand All @@ -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:
*
* <pre class="DocPage-codeBlock mx-6 mb-7">
* (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(
* \<Heading>{errorMessages[errorType]}\</Heading>
* );
* }
* </pre>
*/
export class Dropdown extends React.Component<DropdownProps, DropdownState> {
staticLimit: number;
Expand Down Expand Up @@ -282,6 +300,7 @@ export class Dropdown extends React.Component<DropdownProps, DropdownState> {
selected: _showSelectedItems(async, '', withCheckbox) ? selected : [],
triggerLabel: this.updateTriggerLabel(selectedGroup, optionsLength),
selectAll: getSelectAll(selectedGroup, optionsLength, disabledOptions.length),
errorType: 'DEFAULT',
};
}

Expand Down Expand Up @@ -384,7 +403,7 @@ export class Dropdown extends React.Component<DropdownProps, DropdownState> {
};

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;
Expand All @@ -409,6 +428,7 @@ export class Dropdown extends React.Component<DropdownProps, DropdownState> {

this.setState({
...this.state,
errorType: fetchOptions && errorType !== 'NO_RECORDS_FOUND' ? 'FAILED_TO_FETCH' : errorType,
scrollIndex: res.scrollToIndex || 0,
optionsLength,
loading: false,
Expand All @@ -433,6 +453,7 @@ export class Dropdown extends React.Component<DropdownProps, DropdownState> {
loading: true,
searchInit: true,
searchTerm: search,
errorType: 'NO_RECORDS_FOUND',
});
};

Expand Down Expand Up @@ -633,6 +654,17 @@ export class Dropdown extends React.Component<DropdownProps, DropdownState> {
);
});

reload = () => {
this.setState(
{
loading: true,
},
() => {
this.updateOptions(false);
}
);
};

debounceClear = debounce(250, () => this.updateOptions(false));

onClearOptions = () => {
Expand Down Expand Up @@ -757,6 +789,7 @@ export class Dropdown extends React.Component<DropdownProps, DropdownState> {
triggerLabel,
previousSelected,
scrollIndex,
errorType,
} = this.state;

const { withSelectAll = true, withCheckbox } = this.props;
Expand Down Expand Up @@ -796,6 +829,8 @@ export class Dropdown extends React.Component<DropdownProps, DropdownState> {
onSelectAll={this.onSelectAll}
customTrigger={triggerOptions.customTrigger}
scrollIndex={scrollIndex}
updateOptions={this.reload}
errorType={errorType}
{...rest}
/>
);
Expand Down
56 changes: 44 additions & 12 deletions core/components/atoms/dropdown/DropdownList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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<ErrorTemplateProps>;
/**
* Label of Select All checkbox
* @default "Select All"
Expand Down Expand Up @@ -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<ErrorTemplateProps>;
}

export const usePrevious = (value: any) => {
Expand Down Expand Up @@ -224,6 +236,10 @@ const DropdownList = (props: OptionsProps) => {
className,
searchPlaceholder = 'Search..',
scrollIndex,
updateOptions,
noResultMessage,
errorType,
loadingOptions,
} = props;

const baseProps = extractBaseProps(props);
Expand All @@ -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) {
Expand All @@ -256,7 +274,6 @@ const DropdownList = (props: OptionsProps) => {
minWidth: minWidth ? minWidth : popperMinWidth,
maxWidth: maxWidth ? maxWidth : '100%',
};

requestAnimationFrame(getMinHeight);

setPopoverStyle(popperWrapperStyle);
Expand Down Expand Up @@ -325,6 +342,16 @@ const DropdownList = (props: OptionsProps) => {
minHeight: minHeight,
};

const defaultErrorTemplate = () => {
return (
<ErrorTemplate
dropdownStyle={{ ...dropdownStyle, minHeight: maxHeight }}
updateOptions={updateOptions}
errorType={errorType}
/>
);
};

const getDropdownSectionClass = (showClearButton?: boolean) => {
return classNames({
['Dropdown-section']: true,
Expand Down Expand Up @@ -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);

Expand All @@ -566,15 +593,18 @@ const DropdownList = (props: OptionsProps) => {
);
}

if (listOptions.length === 0 && !loadingOptions && selected.length <= 0) {
const { noResultMessage = 'No result found' } = props;
return (
<div className="Dropdown-wrapper" style={dropdownStyle} data-test="DesignSystem-Dropdown--errorWrapper">
<div className={'Option'}>
<div className={'Option-subinfo'}>{noResultMessage}</div>
if (isDropdownListBlank) {
if (noResultMessage) {
return (
<div className="Dropdown-wrapper" style={dropdownStyle} data-test="DesignSystem-Dropdown--errorWrapper">
<div className={'Option'}>
<div className={'Option-subinfo'}>{noResultMessage}</div>
</div>
</div>
</div>
);
);
} else {
return errorTemplate && errorTemplate({ errorType });
}
}

return (
Expand Down Expand Up @@ -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
Expand All @@ -703,7 +735,7 @@ const DropdownList = (props: OptionsProps) => {
{...popoverOptions}
data-test="DesignSystem-Dropdown--Popover"
>
{(withSearch || props.async) && renderSearch()}
{enableSearch && renderSearch()}
{renderDropdownSection()}
{showApplyButton && withCheckbox && renderApplyButton()}
</Popover>
Expand Down
51 changes: 51 additions & 0 deletions core/components/atoms/dropdown/ErrorTemplate.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
FAILED_TO_FETCH: 'Failed to fetch data',
NO_RECORDS_FOUND: 'No results found',
DEFAULT: 'No record available',
};

const errorDescription: Record<string, string> = {
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<ErrorTemplateProps> = ({ dropdownStyle, errorType, updateOptions }) => {
return (
<div className="Dropdown-wrapper px-7 d-flex" style={dropdownStyle} data-test="DesignSystem-Dropdown--wrapper">
<div
className="d-flex flex-column justify-content-center align-items-center"
data-test="DesignSystem-Dropdown--errorWrapper"
>
<Text className="mb-3" weight="strong">
{errorTitle[errorType]}
</Text>
<Text className="text-align-center mb-6" weight="medium" size="small" appearance="subtle">
{errorDescription[errorType]}
</Text>
{errorType === 'FAILED_TO_FETCH' && (
<Button
size="tiny"
largeIcon={true}
aria-label="reload"
icon="refresh"
iconAlign="left"
onClick={() => updateOptions()}
>
Reload
</Button>
)}
</div>
</div>
);
};
11 changes: 11 additions & 0 deletions core/components/atoms/dropdown/__stories__/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,14 @@ export const fetchOptions = (searchTerm: string, options = dropdownOptions) => {
}, 1000);
});
};

export const fetchEmptyOptions = () => {
return new Promise<fetchOptionSchema>((resolve) => {
window.setTimeout(() => {
resolve({
options: [],
count: 0,
});
}, 1000);
});
};
Loading

0 comments on commit 3f84aa6

Please sign in to comment.