|
| 1 | +import { useClickAway } from 'ahooks'; |
| 2 | +import cx from 'classnames'; |
| 3 | +import uniqueId from 'lodash/uniqueId'; |
| 4 | +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; |
| 5 | +import { CaretDownSmallIcon, CaretUpSmallIcon } from 'tdesign-icons-react'; |
| 6 | +import { Button, Checkbox, Popup, RadioGroup } from 'tdesign-mobile-react'; |
| 7 | +import useDefault from '../_util/useDefault'; |
| 8 | +import parseTNode from '../_util/parseTNode'; |
| 9 | +import CheckboxGroup from '../checkbox/CheckboxGroup'; |
| 10 | +import { StyledProps } from '../common'; |
| 11 | +import useDefaultProps from '../hooks/useDefaultProps'; |
| 12 | +import useConfig from '../hooks/useConfig'; |
| 13 | +import { usePrefixClass } from '../hooks/useClass'; |
| 14 | +import { dropdownItemDefaultProps } from './defaultProps'; |
| 15 | +import DropdownMenuContext from './DropdownMenuContext'; |
| 16 | +import type { TdDropdownItemProps } from './type'; |
| 17 | + |
| 18 | +export interface DropdownItemProps extends TdDropdownItemProps, StyledProps { |
| 19 | + children?: React.ReactNode; |
| 20 | +} |
| 21 | + |
| 22 | +const DropdownItem: React.FC<DropdownItemProps> = (props) => { |
| 23 | + const { |
| 24 | + className, |
| 25 | + children, |
| 26 | + style, |
| 27 | + disabled, |
| 28 | + options: inputOptions, |
| 29 | + optionsColumns, |
| 30 | + placement, |
| 31 | + label, |
| 32 | + value, |
| 33 | + defaultValue, |
| 34 | + onChange, |
| 35 | + multiple, |
| 36 | + onConfirm, |
| 37 | + onReset, |
| 38 | + footer, |
| 39 | + keys, |
| 40 | + } = useDefaultProps<DropdownItemProps>(props, dropdownItemDefaultProps); |
| 41 | + const { classPrefix } = useConfig(); |
| 42 | + const dropdownMenuClass = usePrefixClass('dropdown-menu'); |
| 43 | + const dropdownItemClass = usePrefixClass('dropdown-item'); |
| 44 | + |
| 45 | + const [innerValue, setInnerValue] = useDefault(value, defaultValue, onChange); |
| 46 | + const [modalValue, setModalValue] = useState(innerValue); |
| 47 | + |
| 48 | + const options = useMemo( |
| 49 | + () => |
| 50 | + inputOptions.map((item) => ({ |
| 51 | + value: item[keys?.value ?? 'value'], |
| 52 | + label: item[keys?.label ?? 'label'], |
| 53 | + disabled: item[keys?.disabled ?? 'disabled'], |
| 54 | + })), |
| 55 | + [keys, inputOptions], |
| 56 | + ); |
| 57 | + |
| 58 | + const [id] = useState(() => uniqueId()); |
| 59 | + |
| 60 | + const { direction, activedId, onChangeActivedId, showOverlay, zIndex, closeOnClickOverlay } = |
| 61 | + useContext(DropdownMenuContext); |
| 62 | + |
| 63 | + const labelText = useMemo( |
| 64 | + () => label || options.find((item) => item.value === innerValue)?.label || '', |
| 65 | + [options, label, innerValue], |
| 66 | + ); |
| 67 | + |
| 68 | + const isActived = id === activedId; |
| 69 | + |
| 70 | + const menuItemRef = useRef<HTMLDivElement>(); |
| 71 | + const itemRef = useRef<HTMLDivElement>(); |
| 72 | + |
| 73 | + const getDropdownItemStyle = () => { |
| 74 | + const ele = menuItemRef.current; |
| 75 | + if (!ele) { |
| 76 | + return {}; |
| 77 | + } |
| 78 | + |
| 79 | + const { top, bottom } = ele.getBoundingClientRect(); |
| 80 | + |
| 81 | + if (direction === 'up') { |
| 82 | + return { |
| 83 | + zIndex, |
| 84 | + bottom: `calc(100vh - ${top}px)`, |
| 85 | + }; |
| 86 | + } |
| 87 | + |
| 88 | + return { |
| 89 | + zIndex, |
| 90 | + top: `${bottom}px`, |
| 91 | + }; |
| 92 | + }; |
| 93 | + |
| 94 | + useClickAway(() => { |
| 95 | + if (!isActived || !closeOnClickOverlay) { |
| 96 | + return; |
| 97 | + } |
| 98 | + onChangeActivedId(''); |
| 99 | + }, itemRef); |
| 100 | + |
| 101 | + useEffect(() => { |
| 102 | + if (isActived) { |
| 103 | + setModalValue(innerValue); |
| 104 | + } |
| 105 | + }, [isActived, innerValue]); |
| 106 | + |
| 107 | + const attach = useCallback(() => itemRef.current || document.body, []); |
| 108 | + |
| 109 | + return ( |
| 110 | + <> |
| 111 | + <div |
| 112 | + ref={menuItemRef} |
| 113 | + className={cx(`${dropdownMenuClass}__item`, { |
| 114 | + [`${dropdownMenuClass}__item--active`]: isActived, |
| 115 | + [`${dropdownMenuClass}__item--disabled`]: disabled, |
| 116 | + })} |
| 117 | + onClick={(e) => { |
| 118 | + if (disabled) { |
| 119 | + return; |
| 120 | + } |
| 121 | + onChangeActivedId(isActived ? '' : id); |
| 122 | + if (!isActived) { |
| 123 | + e.stopPropagation(); |
| 124 | + } |
| 125 | + }} |
| 126 | + > |
| 127 | + <div className={`${dropdownMenuClass}__title`}>{labelText}</div> |
| 128 | + {direction === 'down' ? ( |
| 129 | + <CaretDownSmallIcon |
| 130 | + className={cx(`${dropdownMenuClass}__icon`, { |
| 131 | + [`${dropdownMenuClass}__icon--active`]: isActived, |
| 132 | + })} |
| 133 | + /> |
| 134 | + ) : ( |
| 135 | + <CaretUpSmallIcon |
| 136 | + className={cx(`${dropdownMenuClass}__icon`, { |
| 137 | + [`${dropdownMenuClass}__icon--active`]: isActived, |
| 138 | + })} |
| 139 | + /> |
| 140 | + )} |
| 141 | + </div> |
| 142 | + {isActived ? ( |
| 143 | + <div |
| 144 | + key={id} |
| 145 | + className={cx(dropdownItemClass, className)} |
| 146 | + style={{ |
| 147 | + ...style, |
| 148 | + ...getDropdownItemStyle(), |
| 149 | + }} |
| 150 | + ref={itemRef} |
| 151 | + > |
| 152 | + <Popup |
| 153 | + attach={attach} |
| 154 | + visible={isActived} |
| 155 | + placement={direction === 'up' ? 'bottom' : 'top'} |
| 156 | + closeOnOverlayClick={closeOnClickOverlay} |
| 157 | + showOverlay={showOverlay} |
| 158 | + zIndex={zIndex} |
| 159 | + style={{ |
| 160 | + position: 'absolute', |
| 161 | + overflow: 'hidden', |
| 162 | + }} |
| 163 | + overlayProps={{ |
| 164 | + style: { |
| 165 | + position: 'absolute', |
| 166 | + }, |
| 167 | + }} |
| 168 | + onVisibleChange={(visible) => { |
| 169 | + if (!visible) { |
| 170 | + onChangeActivedId(''); |
| 171 | + } |
| 172 | + }} |
| 173 | + > |
| 174 | + <div className={cx(`${dropdownItemClass}__content`, `${classPrefix}-popup__content`)}> |
| 175 | + <div className={cx(`${dropdownItemClass}__body`)}> |
| 176 | + {parseTNode(children) || ( |
| 177 | + <> |
| 178 | + {multiple ? ( |
| 179 | + <CheckboxGroup |
| 180 | + value={modalValue as (string | number)[]} |
| 181 | + onChange={(value) => { |
| 182 | + setModalValue(value as (string | number)[]); |
| 183 | + }} |
| 184 | + className={`${dropdownItemClass}__checkbox-group`} |
| 185 | + style={{ |
| 186 | + gridTemplateColumns: `repeat(${optionsColumns}, 1fr)`, |
| 187 | + }} |
| 188 | + > |
| 189 | + {options.map((item, index) => ( |
| 190 | + <Checkbox |
| 191 | + key={`${item.value}-${index}`} |
| 192 | + className={`${dropdownItemClass}__checkbox-item t-checkbox--tag`} |
| 193 | + icon={false} |
| 194 | + borderless |
| 195 | + value={item.value as string | number} |
| 196 | + label={item.label} |
| 197 | + disabled={item.disabled} |
| 198 | + /> |
| 199 | + ))} |
| 200 | + </CheckboxGroup> |
| 201 | + ) : ( |
| 202 | + <RadioGroup |
| 203 | + className={`${dropdownItemClass}__radio-group`} |
| 204 | + icon="line" |
| 205 | + options={options} |
| 206 | + placement={placement} |
| 207 | + value={modalValue as string | number} |
| 208 | + onChange={(value: string | number) => { |
| 209 | + setModalValue(value); |
| 210 | + setInnerValue(value); |
| 211 | + onChangeActivedId(''); |
| 212 | + }} |
| 213 | + /> |
| 214 | + )} |
| 215 | + </> |
| 216 | + )} |
| 217 | + </div> |
| 218 | + {parseTNode(footer) || |
| 219 | + (multiple && ( |
| 220 | + <div className={`${dropdownItemClass}__footer`}> |
| 221 | + <Button |
| 222 | + disabled={Array.isArray(modalValue) && modalValue.length === 0} |
| 223 | + theme="light" |
| 224 | + className={`${dropdownItemClass}__footer-btn ${dropdownItemClass}__reset-btn`} |
| 225 | + onClick={() => { |
| 226 | + if (typeof onReset === 'function') { |
| 227 | + onReset(modalValue); |
| 228 | + } else { |
| 229 | + setModalValue(innerValue); |
| 230 | + } |
| 231 | + }} |
| 232 | + > |
| 233 | + 重置 |
| 234 | + </Button> |
| 235 | + <Button |
| 236 | + disabled={Array.isArray(modalValue) && modalValue.length === 0} |
| 237 | + theme="primary" |
| 238 | + className={`${dropdownItemClass}__footer-btn ${dropdownItemClass}__confirm-btn`} |
| 239 | + onClick={() => { |
| 240 | + if (typeof onConfirm === 'function') { |
| 241 | + onConfirm(modalValue); |
| 242 | + } else { |
| 243 | + setInnerValue(modalValue); |
| 244 | + } |
| 245 | + onChangeActivedId(''); |
| 246 | + }} |
| 247 | + > |
| 248 | + 确定 |
| 249 | + </Button> |
| 250 | + </div> |
| 251 | + ))} |
| 252 | + </div> |
| 253 | + </Popup> |
| 254 | + </div> |
| 255 | + ) : null} |
| 256 | + </> |
| 257 | + ); |
| 258 | +}; |
| 259 | + |
| 260 | +DropdownItem.displayName = 'DropdownItem'; |
| 261 | + |
| 262 | +export default DropdownItem; |
0 commit comments