) => {
+ const groupKey = group.key;
+ const expanded = !foldedGroupKeys.includes(groupKey);
+
+ return (
+ handleGroupTitleClick(group.key, e)}
+ >
+ {renderOptions(group.options)}
+
+ );
+ },
+ [foldedGroupKeys, handleGroupTitleClick, renderMenuGroup, renderOptions]
+ );
+
+ const renderOptionGroups = useCallback(
+ (groupKey: string) => {
+ const groups = groupOptions(options, groupKey, sort?.(false), sort?.(true));
+ return groups.map(group => renderOptionGroup(group));
+ },
+ [options, renderOptionGroup, sort]
+ );
+
+ const renderVirtualizedOptions = useCallback(() => {
+ return (
+
+ {({ height }) => (
+
+ {({ index }) => renderOption(options[index])}
+
+ )}
+
+ );
+ }, [listProps, maxHeight, options, renderOption, rowHeight, virtualizedListRef]);
+
+ // Example of rendering option groups in VariableSizeList
+ // https://github.com/bvaughn/react-window/issues/358
+ const renderVirtualizedOptionGroups = useCallback(
+ (groupKey: string) => {
+ const groups = groupOptions(options, groupKey, sort?.(false), sort?.(true));
+ return (
+
+ {({ height }) => (
+ {
+ const item = groups[index];
+
+ const expanded = !foldedGroupKeys.includes(item.key);
+ if (expanded) {
+ return item.options.length * rowHeight + rowGroupHeight;
+ }
+
+ return rowGroupHeight;
+ }}
+ {...listProps}
+ >
+ {({ index }) => renderOptionGroup(groups[index])}
+
+ )}
+
+ );
+ },
+ [
+ foldedGroupKeys,
+ listProps,
+ maxHeight,
+ options,
+ renderOptionGroup,
+ rowGroupHeight,
+ rowHeight,
+ sort,
+ virtualizedListRef
+ ]
+ );
+
+ return (
+
+ {typeof groupBy === 'undefined'
+ ? virtualized
+ ? renderVirtualizedOptions()
+ : renderOptions(options)
+ : virtualized
+ ? renderVirtualizedOptionGroups(groupBy)
+ : renderOptionGroups(groupBy)}
+
+ );
+}) as ListboxComponent;
+
+export default Listbox;
diff --git a/src/SelectPicker/ListboxOption.tsx b/src/SelectPicker/ListboxOption.tsx
new file mode 100644
index 0000000000..e1a59f9ca1
--- /dev/null
+++ b/src/SelectPicker/ListboxOption.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { useClassNames } from '../utils';
+import { StandardProps } from '../@types/common';
+
+interface ListboxOptionProps extends StandardProps, React.HTMLAttributes {
+ selected?: boolean;
+ disabled?: boolean;
+ active?: boolean;
+ title?: string;
+ onKeyDown?: (event: React.KeyboardEvent) => void;
+}
+
+const ListboxOption = React.forwardRef(function ListboxOption(
+ props,
+ ref
+) {
+ const {
+ selected,
+ classPrefix = 'dropdown-menu-item',
+ children,
+ className,
+ disabled,
+ active,
+ onKeyDown,
+ ...rest
+ } = props;
+
+ const { withClassPrefix } = useClassNames(classPrefix);
+ const classes = withClassPrefix({ active: selected, focus: active, disabled });
+
+ return (
+
+ {children}
+
+ );
+});
+ListboxOption.displayName = 'Listbox.Option';
+
+export default ListboxOption;
diff --git a/src/SelectPicker/ListboxOptionGroup.tsx b/src/SelectPicker/ListboxOptionGroup.tsx
new file mode 100644
index 0000000000..fdf37ad547
--- /dev/null
+++ b/src/SelectPicker/ListboxOptionGroup.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import classNames from 'classnames';
+import { useClassNames } from '../utils';
+import { StandardProps } from '../@types/common';
+import ArrowDown from '@rsuite/icons/legacy/ArrowDown';
+import useUniqueId from '../utils/useUniqueId';
+
+interface ListboxOptionGroupProps
+ extends StandardProps,
+ Omit, 'title'> {
+ title?: React.ReactNode;
+ expanded?: boolean;
+ onClickTitle?: React.MouseEventHandler;
+}
+
+const ListboxOptionGroup = React.forwardRef(
+ (props, ref) => {
+ const {
+ classPrefix = 'dropdown-menu-group',
+ title,
+ children,
+ className,
+ expanded = true,
+ onClickTitle,
+ ...rest
+ } = props;
+ const { withClassPrefix, prefix, merge } = useClassNames(classPrefix);
+ const classes = merge(className, withClassPrefix());
+
+ const groupId = useUniqueId('listbox-group-');
+ const labelId = groupId + '-label';
+
+ return (
+
+ );
+ }
+);
+ListboxOptionGroup.displayName = 'Listbox.OptionGroup';
+
+export default ListboxOptionGroup;
diff --git a/src/SelectPicker/SelectPicker.tsx b/src/SelectPicker/SelectPicker.tsx
index dd01d4e784..8ec8afc203 100644
--- a/src/SelectPicker/SelectPicker.tsx
+++ b/src/SelectPicker/SelectPicker.tsx
@@ -1,7 +1,6 @@
import React, { useRef, useState, useCallback, Ref } from 'react';
import PropTypes from 'prop-types';
import pick from 'lodash/pick';
-import isUndefined from 'lodash/isUndefined';
import isNil from 'lodash/isNil';
import isFunction from 'lodash/isFunction';
import omit from 'lodash/omit';
@@ -14,10 +13,7 @@ import {
mergeRefs,
shallowEqual
} from '../utils';
-import { getDataGroupBy } from '../utils/getDataGroupBy';
import {
- DropdownMenu,
- DropdownMenuItem,
PickerToggle,
PickerToggleTrigger,
PickerOverlay,
@@ -39,6 +35,7 @@ import {
import { ListProps } from '../Windowing';
import { FormControlPickerProps, ItemDataType } from '../@types/common';
import { ListHandle } from '../Windowing';
+import Listbox from './Listbox';
export interface SelectProps {
/** Set group condition key in data */
@@ -62,6 +59,8 @@ export interface SelectProps {
searchBy?: (keyword: string, label: React.ReactNode, item: ItemDataType) => boolean;
/** Sort options */
+ // TODO-Doma
+ // Deprecate sort(false). Data should be sorted before passed to the component.
sort?: (isGroup: boolean) => (a: any, b: any) => number;
/** Customizing the Rendering Menu list */
@@ -341,43 +340,36 @@ const SelectPicker = React.forwardRef(
const { left, top, className } = positionProps;
const classes = merge(className, menuClassName, prefix('select-menu'));
const styles = { ...menuStyle, left, top };
- let items = filteredData;
- // Create a tree structure data when set `groupBy`
- if (groupBy) {
- items = getDataGroupBy(items, groupBy, sort);
- } else if (typeof sort === 'function') {
- items = items.sort(sort(false));
- }
+ const menu = (() => {
+ if (!filteredData.length) {
+ return {locale?.noResultsText}
;
+ }
- const menu = items.length ? (
-
- ) : (
- {locale?.noResultsText}
- );
+ return (
+ option[valueKey]}
+ id={id ? `${id}-listbox` : undefined}
+ listProps={listProps}
+ listRef={listRef}
+ disabledOptionKeys={disabledItemValues as any[]}
+ labelKey={labelKey}
+ renderMenuGroup={renderMenuGroup}
+ renderMenuItem={renderMenuItem}
+ maxHeight={menuMaxHeight}
+ classPrefix={'picker-select-menu'}
+ optionClassPrefix={'picker-select-menu-item'}
+ selectedOptionKey={value as any}
+ activeOptionKey={focusItemValue as any}
+ groupBy={groupBy}
+ sort={sort}
+ onSelect={handleItemSelect}
+ onGroupTitleClick={onGroupTitleClick}
+ virtualized={virtualized}
+ />
+ );
+ })();
return (
.rs-picker-select-menu-item {
- padding-left: 26px;
- }
}
// Menu item (the option)
@@ -45,3 +41,11 @@
padding-left: @picker-group-children-padding-left;
}
}
+
+.rs-picker-menu-group .rs-picker-select-menu-item {
+ padding-left: @picker-group-children-padding-left;
+}
+
+.rs-picker-menu-group.folded [role='option'] {
+ display: none;
+}
diff --git a/src/Windowing/List.tsx b/src/Windowing/List.tsx
index c1757c8bd2..74b0eb1371 100644
--- a/src/Windowing/List.tsx
+++ b/src/Windowing/List.tsx
@@ -11,7 +11,7 @@ import { useCustom } from '../utils';
export interface ListProps extends WithAsProps {
/**
- * @deprecated use itemSize instead
+ * @deprecated use {@link itemSize} instead
* Either a fixed row height (number) or a function that returns the height of a row given its index: ({ index: number }): number
*/
rowHeight?: number | (({ index: number }) => number);
@@ -47,7 +47,8 @@ export interface ListProps extends WithAsProps {
onScroll?: (props: ListOnScrollProps) => void;
}
-export interface ListHandle extends Partial {
+export interface ListHandle
+ extends Pick {
/**
* @deprecated use scrollToItem instead
* Ensure row is visible. This method can be used to safely scroll back to a cell that a user has scrolled away from even if it was previously scrolled to.
diff --git a/src/locales/index.ts b/src/locales/index.ts
index a165e61af7..7408adb5b0 100644
--- a/src/locales/index.ts
+++ b/src/locales/index.ts
@@ -21,6 +21,7 @@ export { default as zhTW } from './zh_TW';
export { default as faIR } from './fa_IR';
export { default as frFR } from './fr_FR';
export { default as jaJP } from './ja_JP';
+export { default as neNP } from './ne_NP';
type PickKeys = {
[keys in keyof T]?: T[keys];
diff --git a/src/locales/ne_NP.ts b/src/locales/ne_NP.ts
new file mode 100644
index 0000000000..b148b39434
--- /dev/null
+++ b/src/locales/ne_NP.ts
@@ -0,0 +1,82 @@
+import enGB from 'date-fns/locale/en-GB';
+
+const Calendar = {
+ sunday: 'आ',
+ monday: 'सो',
+ tuesday: 'म',
+ wednesday: 'बु',
+ thursday: 'बि',
+ friday: 'शु',
+ saturday: 'श',
+ ok: 'हुन्छ',
+ today: 'आज',
+ yesterday: 'हिजो',
+ hours: 'घण्टा',
+ minutes: 'मिनेट',
+ seconds: 'सेकेन्ड',
+ /**
+ * Format of the string is based on Unicode Technical Standard #35:
+ * https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
+ **/
+ formattedMonthPattern: 'MMM yyyy',
+ formattedDayPattern: 'dd MMM yyyy',
+ dateLocale: enGB as any
+};
+
+export default {
+ common: {
+ loading: 'लोड हुँदैछ...',
+ emptyMessage: 'कुनै डाटा छैन'
+ },
+ Plaintext: {
+ unfilled: 'भरिएको छैन',
+ notSelected: 'चयन गरिएको छैन',
+ notUploaded: 'अपलोड गरिएको छैन'
+ },
+ Pagination: {
+ more: 'थप',
+ prev: 'अघिल्लो',
+ next: 'अर्को',
+ first: 'पहिलो',
+ last: 'अन्तिम',
+ limit: '{0} / पृष्ठ',
+ total: 'कुल पङ्क्तिहरू: {0}',
+ skip: '{0} पृष्ठमा जानुहोस्'
+ },
+ Calendar,
+ DatePicker: {
+ ...Calendar
+ },
+ DateRangePicker: {
+ ...Calendar,
+ last7Days: 'पछिल्लो ७ दिन'
+ },
+ Picker: {
+ noResultsText: 'कुनै परिणाम फेला परेन',
+ placeholder: 'चयन गर्नुहोस्',
+ searchPlaceholder: 'खोजी गर्नुहोस्',
+ checkAll: 'सबै'
+ },
+ InputPicker: {
+ newItem: 'नयाँ थप्नुहोस्',
+ createOption: 'विकल्प सिर्जना गर्नुहोस् "{0}"'
+ },
+ Uploader: {
+ inited: 'प्रारम्भिक',
+ progress: 'अपलोड गर्दै',
+ error: 'त्रुटि भयो',
+ complete: 'समाप्त',
+ emptyFile: 'खाली',
+ upload: 'अपलोड गर्नुहोस्'
+ },
+ CloseButton: {
+ closeLabel: 'बन्द गर्नुहोस्'
+ },
+ Breadcrumb: {
+ expandText: 'स्थान देखाउनुहोस्'
+ },
+ Toggle: {
+ on: 'खोल्नुहोस्',
+ off: 'बन्द गर्नुहोस्'
+ }
+};
diff --git a/src/styles/mixins/listbox.less b/src/styles/mixins/listbox.less
index 38d76d8c11..711c7f0591 100644
--- a/src/styles/mixins/listbox.less
+++ b/src/styles/mixins/listbox.less
@@ -32,6 +32,8 @@
font-weight: normal;
line-height: @line-height-base;
color: var(--rs-text-primary);
+ // FIXME-Doma
+ // No need to use pointer, just use default
cursor: pointer;
text-decoration: none;
width: 100%;
diff --git a/src/utils/getDataGroupBy.ts b/src/utils/getDataGroupBy.ts
index f94fa02ebf..f08185f61b 100644
--- a/src/utils/getDataGroupBy.ts
+++ b/src/utils/getDataGroupBy.ts
@@ -5,6 +5,10 @@ const hasSymbol = typeof Symbol === 'function';
export const KEY_GROUP = hasSymbol ? Symbol('_$grouped') : '_$grouped';
export const KEY_GROUP_TITLE = 'groupTitle';
+/**
+ * Chunk data into groups
+ * @returns [group, child, child, group, child, child]
+ */
export function getDataGroupBy(
data: readonly T[],
key: string,
@@ -28,3 +32,33 @@ export function getDataGroupBy(
// rather than [group, group, child, child, child, child]
return flattenTree(groups, group => group.children, WalkTreeStrategy.DFS);
}
+
+/**
+ * Chunk options into groups
+ * @returns [
+ * group {
+ * key
+ * options
+ * }
+ * group {
+ * key
+ * options
+ * }
+ * ]
+ */
+export type Group = { key: string; options: T[] };
+export type CompareFn = (a: T, b: T) => number;
+export function groupOptions(
+ options: readonly T[],
+ groupKey: string,
+ compareOptions?: CompareFn,
+ compareGroups?: CompareFn>
+): Group[] {
+ const groupMap = _.groupBy(options, groupKey);
+ const groups = Object.entries(groupMap).map(([key, options]) => ({
+ key,
+ options: typeof compareOptions === 'function' ? options.sort(compareOptions) : options
+ }));
+
+ return typeof compareGroups === 'function' ? groups.sort(compareGroups) : groups;
+}
diff --git a/src/utils/treeUtils.ts b/src/utils/treeUtils.ts
index 678579ce3c..f64ea16819 100644
--- a/src/utils/treeUtils.ts
+++ b/src/utils/treeUtils.ts
@@ -930,11 +930,23 @@ export function useTreeSearch(props: TreeSearchProps) {
);
// Use search keywords to filter options.
- const [searchKeywordState, setSearchKeyword] = useState(() => searchKeyword ?? '');
+ const [searchKeywordState, setSearchKeyword] = useState(searchKeyword ?? '');
const [filteredData, setFilteredData] = useState(() =>
filterVisibleData(data, searchKeywordState)
);
+ const handleSearch = (searchKeyword: string, event?: React.ChangeEvent) => {
+ const filteredData = filterVisibleData(data, searchKeyword);
+ setFilteredData(filteredData);
+ setSearchKeyword(searchKeyword);
+ event && callback?.(searchKeyword, filteredData, event);
+ };
+
+ useEffect(() => {
+ handleSearch(searchKeyword ?? '');
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [searchKeyword]);
+
const handleSetFilteredData = useCallback(
(data: T[], searchKeyword: string) => {
setFilteredData(filterVisibleData(data, searchKeyword));
@@ -942,13 +954,6 @@ export function useTreeSearch(props: TreeSearchProps) {
[filterVisibleData]
);
- const handleSearch = (searchKeyword: string, event: React.ChangeEvent) => {
- const filteredData = filterVisibleData(data, searchKeyword);
- setFilteredData(filteredData);
- setSearchKeyword(searchKeyword);
- callback?.(searchKeyword, filteredData, event);
- };
-
return {
searchKeywordState,
filteredData,