ColumnFilter.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import React, { isValidElement, useEffect, useState } from 'react';
  2. import cls from 'classnames';
  3. import { isEqual, noop, pick } from 'lodash';
  4. import { IconFilter } from '@douyinfe/semi-icons';
  5. import { cssClasses } from '@douyinfe/semi-foundation/table/constants';
  6. import Dropdown, { DropdownProps } from '../dropdown';
  7. import { Radio } from '../radio';
  8. import { Checkbox } from '../checkbox';
  9. import {
  10. FilterIcon,
  11. Filter,
  12. OnFilterDropdownVisibleChange,
  13. RenderFilterDropdownItem
  14. } from './interface';
  15. function renderDropdown(props: RenderDropdownProps, nestedElem: React.ReactNode = null, level = 0) {
  16. const {
  17. filterMultiple = true,
  18. filters = [],
  19. filteredValue = [],
  20. filterDropdownVisible,
  21. onSelect = noop,
  22. onFilterDropdownVisibleChange = noop,
  23. trigger = 'click',
  24. position = 'bottom',
  25. renderFilterDropdown,
  26. renderFilterDropdownItem,
  27. } = props ?? {};
  28. const renderFilterDropdownProps: RenderFilterDropdownProps = pick(props, ['tempFilteredValue', 'setTempFilteredValue', 'confirm', 'clear', 'close', 'filters']);
  29. const render = typeof renderFilterDropdown === 'function' ? renderFilterDropdown(renderFilterDropdownProps) : (
  30. <Dropdown.Menu>
  31. {Array.isArray(filters) &&
  32. filters.map((filter, index) => {
  33. const changeFn = (e: React.MouseEvent<HTMLLIElement>) => {
  34. const domEvent = e && e.nativeEvent;
  35. if (domEvent) {
  36. // Block this event to prevent the pop-up layer from closing
  37. domEvent.stopImmediatePropagation();
  38. // Prevent bubbling and default events to prevent label click events from triggering twice
  39. domEvent.stopPropagation();
  40. domEvent.preventDefault();
  41. }
  42. let values = [...filteredValue];
  43. const included = values.includes(filter.value);
  44. const idx = values.indexOf(filter.value);
  45. if (idx > -1) {
  46. values.splice(idx, 1);
  47. } else if (filterMultiple) {
  48. values.push(filter.value);
  49. } else {
  50. values = [filter.value];
  51. }
  52. return onSelect({
  53. value: filter.value,
  54. filteredValue: values,
  55. included: !included,
  56. domEvent,
  57. });
  58. };
  59. const checked = filteredValue.includes(filter.value);
  60. const { text } = filter;
  61. const { value } = filter;
  62. const key = `${level}_${index}`;
  63. const dropdownItem =
  64. typeof renderFilterDropdownItem === 'function' ?
  65. renderFilterDropdownItem({
  66. onChange: changeFn,
  67. filterMultiple,
  68. value,
  69. text,
  70. checked,
  71. filteredValue,
  72. level,
  73. }) :
  74. null;
  75. let item =
  76. dropdownItem && React.isValidElement(dropdownItem) ? (
  77. React.cloneElement(dropdownItem, { key })
  78. ) : (
  79. <Dropdown.Item key={key} onClick={changeFn}>
  80. {filterMultiple ? (
  81. <Checkbox checked={checked}>{text}</Checkbox>
  82. ) : (
  83. <Radio checked={checked}>{text}</Radio>
  84. )}
  85. </Dropdown.Item>
  86. );
  87. if (Array.isArray(filter.children) && filter.children.length) {
  88. const childrenDropdownProps = {
  89. ...props,
  90. filters: filter.children,
  91. trigger: 'hover' as const,
  92. position: 'right' as const,
  93. };
  94. delete childrenDropdownProps.filterDropdownVisible;
  95. item = renderDropdown(childrenDropdownProps, item, level + 1);
  96. }
  97. return item;
  98. })}
  99. </Dropdown.Menu>
  100. );
  101. const dropdownProps: DropdownProps = {
  102. ...props,
  103. onVisibleChange: (visible: boolean) => onFilterDropdownVisibleChange(visible),
  104. trigger,
  105. position,
  106. render,
  107. };
  108. if (filterDropdownVisible != null) {
  109. dropdownProps.visible = filterDropdownVisible;
  110. }
  111. return (
  112. <Dropdown {...dropdownProps} key={`Dropdown_level_${level}`} className={`${cssClasses.PREFIX}-column-filter-dropdown`}>
  113. {nestedElem}
  114. </Dropdown>
  115. );
  116. }
  117. export default function ColumnFilter(props: ColumnFilterProps = {}): React.ReactElement {
  118. const {
  119. prefixCls = cssClasses.PREFIX,
  120. filteredValue,
  121. filterIcon = 'filter',
  122. filterDropdownProps,
  123. onSelect,
  124. filterDropdownVisible,
  125. renderFilterDropdown,
  126. onFilterDropdownVisibleChange
  127. } = props;
  128. let { filterDropdown = null } = props;
  129. // custom filter related status
  130. const isFilterDropdownVisibleControlled = typeof filterDropdownVisible !== 'undefined';
  131. const isCustomFilterDropdown = typeof renderFilterDropdown === 'function';
  132. const isCustomDropdownVisible = !isFilterDropdownVisibleControlled && isCustomFilterDropdown;
  133. const [tempFilteredValue, setTempFilteredValue] = useState<any[]>(filteredValue);
  134. const dropdownVisibleInitValue = isCustomDropdownVisible ? false : filterDropdownVisible;
  135. const [dropdownVisible, setDropdownVisible] = useState<boolean | undefined>(dropdownVisibleInitValue);
  136. useEffect(() => {
  137. if (typeof filterDropdownVisible !== 'undefined') {
  138. setDropdownVisible(filterDropdownVisible);
  139. }
  140. }, [filterDropdownVisible]);
  141. useEffect(() => {
  142. setTempFilteredValue(filteredValue);
  143. }, [filteredValue]);
  144. const confirm: RenderFilterDropdownProps['confirm'] = (props = {}) => {
  145. const newFilteredValue = props?.filteredValue || tempFilteredValue;
  146. if (!isEqual(newFilteredValue, filteredValue)) {
  147. onSelect({ filteredValue: newFilteredValue });
  148. }
  149. if (props.closeDropdown) {
  150. setDropdownVisible(false);
  151. }
  152. };
  153. const clear: RenderFilterDropdownProps['clear'] = (props: { closeDropdown?: boolean } = {}) => {
  154. setTempFilteredValue([]);
  155. onSelect({ filteredValue: [] });
  156. if (props.closeDropdown) {
  157. setDropdownVisible(false);
  158. }
  159. };
  160. const close: RenderFilterDropdownProps['close'] = () => {
  161. setDropdownVisible(false);
  162. };
  163. const handleFilterDropdownVisibleChange = (visible: boolean) => {
  164. if (isCustomDropdownVisible) {
  165. setDropdownVisible(visible);
  166. }
  167. onFilterDropdownVisibleChange(visible);
  168. };
  169. const renderFilterDropdownProps: RenderFilterDropdownProps = {
  170. tempFilteredValue,
  171. setTempFilteredValue,
  172. confirm,
  173. clear,
  174. close
  175. };
  176. const finalCls = cls(`${prefixCls}-column-filter`, {
  177. on: Array.isArray(filteredValue) && filteredValue.length,
  178. });
  179. let iconElem;
  180. if (typeof filterIcon === 'function') {
  181. iconElem = filterIcon(Array.isArray(filteredValue) && filteredValue.length > 0);
  182. } else if (isValidElement(filterIcon)) {
  183. iconElem = filterIcon;
  184. } else {
  185. iconElem = (
  186. <div className={finalCls}>
  187. {'\u200b'/* ZWSP(zero-width space) */}
  188. <IconFilter
  189. role="button"
  190. aria-label="Filter data with this column"
  191. aria-haspopup="listbox"
  192. tabIndex={-1}
  193. size="default"
  194. />
  195. </div>
  196. );
  197. }
  198. const renderProps = {
  199. ...props,
  200. ...filterDropdownProps,
  201. ...renderFilterDropdownProps,
  202. filterDropdownVisible: isFilterDropdownVisibleControlled ? filterDropdownVisible : dropdownVisible,
  203. onFilterDropdownVisibleChange: handleFilterDropdownVisibleChange,
  204. };
  205. filterDropdown = React.isValidElement<ColumnFilterProps>(filterDropdown) ?
  206. filterDropdown :
  207. renderDropdown(renderProps, iconElem);
  208. return filterDropdown;
  209. }
  210. export interface ColumnFilterProps extends Omit<RenderDropdownProps, keyof RenderFilterDropdownProps> {
  211. prefixCls?: string;
  212. filteredValue?: any[];
  213. filterIcon?: FilterIcon;
  214. filterDropdown?: React.ReactElement;
  215. filterDropdownProps?: FilterDropdownProps;
  216. filters?: Filter[]
  217. }
  218. export interface RenderDropdownProps extends FilterDropdownProps, RenderFilterDropdownProps {
  219. filterMultiple?: boolean;
  220. filters?: Filter[];
  221. filteredValue?: any[];
  222. filterDropdownVisible?: boolean;
  223. onSelect?: (data: OnSelectData) => void;
  224. onFilterDropdownVisibleChange?: OnFilterDropdownVisibleChange;
  225. renderFilterDropdown?: (props: RenderFilterDropdownProps) => React.ReactNode;
  226. renderFilterDropdownItem?: RenderFilterDropdownItem
  227. }
  228. export interface FilterDropdownProps extends Omit<DropdownProps, 'render' | 'onVisibleChange'> {}
  229. export interface OnSelectData {
  230. value?: any;
  231. /** only this value is used now */
  232. filteredValue: any;
  233. included?: boolean;
  234. domEvent?: React.MouseEvent<HTMLElement>
  235. }
  236. export interface RenderFilterDropdownProps {
  237. /** temporary filteredValue */
  238. tempFilteredValue: any[];
  239. /** set temporary filteredValue */
  240. setTempFilteredValue: (tempFilteredValue: any[]) => void;
  241. /** set tempFilteredValue to filteredValue. You can also pass filteredValue to directly set the filteredValue */
  242. confirm: (props?: { closeDropdown?: boolean; filteredValue?: any[] }) => void;
  243. /** clear tempFilteredValue and filteredValue */
  244. clear: (props?: { closeDropdown?: boolean }) => void;
  245. /** close dropdown */
  246. close: () => void;
  247. /** column filters */
  248. filters?: RenderDropdownProps['filters']
  249. }