This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
@@ -0,0 +1,14 @@
import * as React from 'react';
import type { SegmentedValue } from '.';
export interface MotionThumbInterface {
containerRef: React.RefObject<HTMLDivElement>;
value: SegmentedValue;
getValueIndex: (value: SegmentedValue) => number;
prefixCls: string;
motionName: string;
onMotionStart: VoidFunction;
onMotionEnd: VoidFunction;
direction?: 'ltr' | 'rtl';
vertical?: boolean;
}
export default function MotionThumb(props: MotionThumbInterface): React.JSX.Element | null;
+174
View File
@@ -0,0 +1,174 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = MotionThumb;
var _motion = _interopRequireDefault(require("@rc-component/motion"));
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _ref = require("@rc-component/util/lib/ref");
var _clsx = require("clsx");
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const calcThumbStyle = (targetElement, vertical) => {
if (!targetElement) return null;
const style = {
left: targetElement.offsetLeft,
right: targetElement.parentElement.clientWidth - targetElement.clientWidth - targetElement.offsetLeft,
width: targetElement.clientWidth,
top: targetElement.offsetTop,
bottom: targetElement.parentElement.clientHeight - targetElement.clientHeight - targetElement.offsetTop,
height: targetElement.clientHeight
};
if (vertical) {
// Adjusts positioning and size for vertical layout by setting horizontal properties to 0 and using vertical properties from the style object.
return {
left: 0,
right: 0,
width: 0,
top: style.top,
bottom: style.bottom,
height: style.height
};
}
return {
left: style.left,
right: style.right,
width: style.width,
top: 0,
bottom: 0,
height: 0
};
};
const toPX = value => value !== undefined ? `${value}px` : undefined;
function MotionThumb(props) {
const {
prefixCls,
containerRef,
value,
getValueIndex,
motionName,
onMotionStart,
onMotionEnd,
direction,
vertical = false
} = props;
const thumbRef = React.useRef(null);
const [prevValue, setPrevValue] = React.useState(value);
// =========================== Effect ===========================
const findValueElement = val => {
const index = getValueIndex(val);
const ele = containerRef.current?.querySelectorAll(`.${prefixCls}-item`)[index];
return ele?.offsetParent && ele;
};
const [prevStyle, setPrevStyle] = React.useState(null);
const [nextStyle, setNextStyle] = React.useState(null);
(0, _useLayoutEffect.default)(() => {
if (prevValue !== value) {
const prev = findValueElement(prevValue);
const next = findValueElement(value);
const calcPrevStyle = calcThumbStyle(prev, vertical);
const calcNextStyle = calcThumbStyle(next, vertical);
setPrevValue(value);
setPrevStyle(calcPrevStyle);
setNextStyle(calcNextStyle);
if (prev && next) {
onMotionStart();
} else {
onMotionEnd();
}
}
}, [value]);
const thumbStart = React.useMemo(() => {
if (vertical) {
return toPX(prevStyle?.top ?? 0);
}
if (direction === 'rtl') {
return toPX(-prevStyle?.right);
}
return toPX(prevStyle?.left);
}, [vertical, direction, prevStyle]);
const thumbActive = React.useMemo(() => {
if (vertical) {
return toPX(nextStyle?.top ?? 0);
}
if (direction === 'rtl') {
return toPX(-nextStyle?.right);
}
return toPX(nextStyle?.left);
}, [vertical, direction, nextStyle]);
// =========================== Motion ===========================
const onAppearStart = () => {
if (vertical) {
return {
transform: 'translateY(var(--thumb-start-top))',
height: 'var(--thumb-start-height)'
};
}
return {
transform: 'translateX(var(--thumb-start-left))',
width: 'var(--thumb-start-width)'
};
};
const onAppearActive = () => {
if (vertical) {
return {
transform: 'translateY(var(--thumb-active-top))',
height: 'var(--thumb-active-height)'
};
}
return {
transform: 'translateX(var(--thumb-active-left))',
width: 'var(--thumb-active-width)'
};
};
const onVisibleChanged = () => {
setPrevStyle(null);
setNextStyle(null);
onMotionEnd();
};
// =========================== Render ===========================
// No need motion when nothing exist in queue
if (!prevStyle || !nextStyle) {
return null;
}
return /*#__PURE__*/React.createElement(_motion.default, {
visible: true,
motionName: motionName,
motionAppear: true,
onAppearStart: onAppearStart,
onAppearActive: onAppearActive,
onVisibleChanged: onVisibleChanged
}, ({
className: motionClassName,
style: motionStyle
}, ref) => {
const mergedStyle = {
...motionStyle,
'--thumb-start-left': thumbStart,
'--thumb-start-width': toPX(prevStyle?.width),
'--thumb-active-left': thumbActive,
'--thumb-active-width': toPX(nextStyle?.width),
'--thumb-start-top': thumbStart,
'--thumb-start-height': toPX(prevStyle?.height),
'--thumb-active-top': thumbActive,
'--thumb-active-height': toPX(nextStyle?.height)
};
// It's little ugly which should be refactor when @umi/test update to latest jsdom
const motionProps = {
ref: (0, _ref.composeRef)(thumbRef, ref),
style: mergedStyle,
className: (0, _clsx.clsx)(`${prefixCls}-thumb`, motionClassName)
};
if (process.env.NODE_ENV === 'test') {
motionProps['data-test-style'] = JSON.stringify(mergedStyle);
}
return /*#__PURE__*/React.createElement("div", motionProps);
});
}
+38
View File
@@ -0,0 +1,38 @@
import * as React from 'react';
export type SemanticName = 'item' | 'label';
export type SegmentedValue = string | number;
export type SegmentedRawOption = SegmentedValue;
export interface SegmentedLabeledOption<ValueType = SegmentedRawOption> {
className?: string;
disabled?: boolean;
label: React.ReactNode;
value: ValueType;
/**
* html `title` property for label
*/
title?: string;
}
type ItemRender = (node: React.ReactNode, info: {
item: SegmentedLabeledOption;
}) => React.ReactNode;
type SegmentedOptions<T = SegmentedRawOption> = (T | SegmentedLabeledOption<T>)[];
export interface SegmentedProps<ValueType = SegmentedValue> extends Omit<React.HTMLProps<HTMLDivElement>, 'defaultValue' | 'value' | 'onChange'> {
options: SegmentedOptions<ValueType>;
defaultValue?: ValueType;
value?: ValueType;
onChange?: (value: ValueType) => void;
disabled?: boolean;
prefixCls?: string;
direction?: 'ltr' | 'rtl';
motionName?: string;
vertical?: boolean;
name?: string;
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
itemRender?: ItemRender;
}
declare const Segmented: React.ForwardRefExoticComponent<Omit<SegmentedProps<SegmentedValue>, "ref"> & React.RefAttributes<HTMLDivElement>>;
declare const TypedSegmented: <ValueType>(props: SegmentedProps<ValueType> & {
ref?: React.ForwardedRef<HTMLDivElement>;
}) => ReturnType<typeof Segmented>;
export default TypedSegmented;
+239
View File
@@ -0,0 +1,239 @@
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _useControlledState = _interopRequireDefault(require("@rc-component/util/lib/hooks/useControlledState"));
var _omit = _interopRequireDefault(require("@rc-component/util/lib/omit"));
var _ref = require("@rc-component/util/lib/ref");
var _clsx = require("clsx");
var React = _interopRequireWildcard(require("react"));
var _MotionThumb = _interopRequireDefault(require("./MotionThumb"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function getValidTitle(option) {
if (typeof option.title !== 'undefined') {
return option.title;
}
// read `label` when title is `undefined`
if (typeof option.label !== 'object') {
return option.label?.toString();
}
}
function normalizeOptions(options) {
return options.map(option => {
if (typeof option === 'object' && option !== null) {
const validTitle = getValidTitle(option);
return {
...option,
title: validTitle
};
}
return {
label: option?.toString(),
title: option?.toString(),
value: option
};
});
}
const InternalSegmentedOption = ({
prefixCls,
className,
style,
styles,
classNames: segmentedClassNames,
data,
disabled,
checked,
label,
title,
value,
name,
onChange,
onFocus,
onBlur,
onKeyDown,
onKeyUp,
onMouseDown,
itemRender = node => node
}) => {
const handleChange = event => {
if (disabled) {
return;
}
onChange(event, value);
};
const itemContent = /*#__PURE__*/React.createElement("label", {
className: (0, _clsx.clsx)(className, {
[`${prefixCls}-item-disabled`]: disabled
}),
style: style,
onMouseDown: onMouseDown
}, /*#__PURE__*/React.createElement("input", {
name: name,
className: `${prefixCls}-item-input`,
type: "radio",
disabled: disabled,
checked: checked,
onChange: handleChange,
onFocus: onFocus,
onBlur: onBlur,
onKeyDown: onKeyDown,
onKeyUp: onKeyUp
}), /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-item-label`, segmentedClassNames?.label),
title: title,
style: styles?.label
}, label));
return itemRender(itemContent, {
item: data
});
};
const Segmented = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls = 'rc-segmented',
direction,
vertical,
options = [],
disabled,
defaultValue,
value,
name,
onChange,
className = '',
style,
styles,
classNames: segmentedClassNames,
motionName = 'thumb-motion',
itemRender,
...restProps
} = props;
const containerRef = React.useRef(null);
const mergedRef = React.useMemo(() => (0, _ref.composeRef)(containerRef, ref), [containerRef, ref]);
const segmentedOptions = React.useMemo(() => {
return normalizeOptions(options);
}, [options]);
// Note: We should not auto switch value when value not exist in options
// which may break single source of truth.
const [rawValue, setRawValue] = (0, _useControlledState.default)(defaultValue ?? segmentedOptions[0]?.value, value);
// ======================= Change ========================
const [thumbShow, setThumbShow] = React.useState(false);
const handleChange = (event, val) => {
setRawValue(val);
onChange?.(val);
};
const divProps = (0, _omit.default)(restProps, ['children']);
// ======================= Focus ========================
const [isKeyboard, setIsKeyboard] = React.useState(false);
const [isFocused, setIsFocused] = React.useState(false);
const handleFocus = () => {
setIsFocused(true);
};
const handleBlur = () => {
setIsFocused(false);
};
const handleMouseDown = () => {
setIsKeyboard(false);
};
// capture keyboard tab interaction for correct focus style
const handleKeyUp = event => {
if (event.key === 'Tab') {
setIsKeyboard(true);
}
};
// ======================= Keyboard ========================
const onOffset = offset => {
const currentIndex = segmentedOptions.findIndex(option => option.value === rawValue);
const total = segmentedOptions.length;
const nextIndex = (currentIndex + offset + total) % total;
const nextOption = segmentedOptions[nextIndex];
if (nextOption) {
setRawValue(nextOption.value);
onChange?.(nextOption.value);
}
};
const handleKeyDown = event => {
switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
onOffset(-1);
break;
case 'ArrowRight':
case 'ArrowDown':
onOffset(1);
break;
}
};
const renderOption = segmentedOption => {
const {
value: optionValue,
disabled: optionDisabled
} = segmentedOption;
return /*#__PURE__*/React.createElement(InternalSegmentedOption, (0, _extends2.default)({}, segmentedOption, {
name: name,
data: segmentedOption,
itemRender: itemRender,
key: optionValue,
prefixCls: prefixCls,
className: (0, _clsx.clsx)(segmentedOption.className, `${prefixCls}-item`, segmentedClassNames?.item, {
[`${prefixCls}-item-selected`]: optionValue === rawValue && !thumbShow,
[`${prefixCls}-item-focused`]: isFocused && isKeyboard && optionValue === rawValue
}),
style: styles?.item,
classNames: segmentedClassNames,
styles: styles,
checked: optionValue === rawValue,
onChange: handleChange,
onFocus: handleFocus,
onBlur: handleBlur,
onKeyDown: handleKeyDown,
onKeyUp: handleKeyUp,
onMouseDown: handleMouseDown,
disabled: !!disabled || !!optionDisabled
}));
};
return /*#__PURE__*/React.createElement("div", (0, _extends2.default)({
role: "radiogroup",
"aria-label": "segmented control",
tabIndex: disabled ? undefined : 0,
"aria-orientation": vertical ? 'vertical' : 'horizontal',
style: style
}, divProps, {
className: (0, _clsx.clsx)(prefixCls, {
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-vertical`]: vertical
}, className),
ref: mergedRef
}), /*#__PURE__*/React.createElement("div", {
className: `${prefixCls}-group`
}, /*#__PURE__*/React.createElement(_MotionThumb.default, {
vertical: vertical,
prefixCls: prefixCls,
value: rawValue,
containerRef: containerRef,
motionName: `${prefixCls}-${motionName}`,
direction: direction,
getValueIndex: val => segmentedOptions.findIndex(n => n.value === val),
onMotionStart: () => {
setThumbShow(true);
},
onMotionEnd: () => {
setThumbShow(false);
}
}), segmentedOptions.map(renderOption)));
});
if (process.env.NODE_ENV !== 'production') {
Segmented.displayName = 'Segmented';
}
const TypedSegmented = Segmented;
var _default = exports.default = TypedSegmented;