mirror of
https://github.com/hanzoai/cd-ui.git
synced 2026-10-30 20:42:45 +00:00
* chore: fix Rules-of-React violations for React Compiler readiness Add eslint-plugin-react-hooks v7 (recommended-latest) and fix all error-level Rules-of-React findings so the React Compiler can optimize the component library. The healthcheck reports 100% of components compilable; these fixes clear the 34 ESLint errors across the src and v2 component trees: - refs: stop accessing ref.current during render (incl. the react-form compat shim, whose public ref-forwarding API is preserved) - set-state-in-effect: convert to derived state or restructure effects - immutability: clone-before-mutate; stop mutating props/state - rules-of-hooks: hoist conditional hook calls to top level All 47 unit tests pass; behavior is preserved. Signed-off-by: Jonathan Winters <[email protected]> * chore: resolve react-hooks deps from public npm registry The eslint-plugin-react-hooks@7 dependency subtree was locked to an internal mirror, which the public CI runner can't reach (ETIMEDOUT). Rewrite the 21 affected resolved URLs to registry.yarnpkg.com. Signed-off-by: Jonathan Winters <[email protected]> * useKeybinding -> registerKeybinding Signed-off-by: Jonathan Winters <[email protected]> * chore: fix Rules-of-React refs violations from registerKeybinding rename The useKeybinding->registerKeybinding rename turned hook calls into plain function calls in the render body; since registerKeybinding takes ref targets, the new react-hooks/refs lint flagged them. Move the calls into useEffect (registration is a side effect) in sliding-panel and the v2 autocomplete. In the v1 autocomplete, the flagged value is a forwarded callback ref, not a render-time read, so suppress that one with a comment. Also dedupe pnpm-lock.yaml. Signed-off-by: Jonathan Winters <[email protected]> --------- Signed-off-by: Jonathan Winters <[email protected]>
118 lines
4.2 KiB
TypeScript
118 lines
4.2 KiB
TypeScript
import * as React from 'react';
|
|
import {Key, useKeyListener} from '../../shared';
|
|
import {useClickOutside, useTimeout} from '../../utils/utils';
|
|
import {EffectDiv} from '../effect-div/effect-div';
|
|
import {Tooltip} from '../tooltip/tooltip';
|
|
|
|
import {Theme} from '../theme-div/theme-div';
|
|
import './action-button.scss';
|
|
|
|
export interface ActionButtonProps {
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
action?: Function;
|
|
label?: string;
|
|
icon?: string;
|
|
indicateLoading?: boolean;
|
|
dark?: boolean;
|
|
theme?: Theme;
|
|
disabled?: boolean;
|
|
short?: boolean;
|
|
style?: React.CSSProperties;
|
|
tooltip?: React.ReactNode;
|
|
shouldConfirm?: boolean;
|
|
indicateSuccess?: boolean;
|
|
transparent?: boolean;
|
|
loading?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Provide users with clickable buttons to perform an action
|
|
*/
|
|
export const ActionButton = (props: ActionButtonProps) => {
|
|
const {label, action, icon, indicateLoading, short, shouldConfirm} = props;
|
|
const [loading, setLoading] = React.useState(false);
|
|
const [success, setSuccess] = React.useState(false);
|
|
const [confirmed, confirm] = React.useState(false);
|
|
const [displayLabel, setDisplayLabel] = React.useState(label);
|
|
const [displayIcon, setDisplayIcon] = React.useState(icon);
|
|
const [prevLabel, setPrevLabel] = React.useState(label);
|
|
const [prevIcon, setPrevIcon] = React.useState(icon);
|
|
const ref = React.useRef(null);
|
|
|
|
// Sync display state to the latest props during render (instead of in an effect)
|
|
// so prop changes take effect immediately without an extra render pass.
|
|
if (props.label !== prevLabel || props.icon !== prevIcon) {
|
|
setPrevLabel(props.label);
|
|
setPrevIcon(props.icon);
|
|
setDisplayLabel(props.label);
|
|
setDisplayIcon(props.icon);
|
|
}
|
|
|
|
const unconfirm = React.useCallback(() => {
|
|
if (props.shouldConfirm) {
|
|
setDisplayIcon(icon);
|
|
setDisplayLabel(label);
|
|
confirm(false);
|
|
}
|
|
}, [icon, label, props.shouldConfirm]);
|
|
useClickOutside(ref, unconfirm);
|
|
|
|
useTimeout(() => setLoading(false), 1000, [loading]);
|
|
useTimeout(() => setSuccess(false), indicateLoading ? 1000 + 500 : 500, [success]);
|
|
|
|
const listen = useKeyListener();
|
|
listen(Key.ESCAPE, () => {
|
|
unconfirm();
|
|
return confirmed;
|
|
});
|
|
|
|
const getIcon = () => {
|
|
if ((loading || props.loading) && indicateLoading) {
|
|
return 'fa-circle-notch';
|
|
} else if (success && props.indicateSuccess) {
|
|
return 'fa-check';
|
|
} else {
|
|
return displayIcon;
|
|
}
|
|
};
|
|
const button = (
|
|
<EffectDiv
|
|
className={`action-button ${props.dark ? 'action-button--dark' : ''} ${props.disabled ? 'action-button--disabled' : ''} ${confirmed ? 'action-button--selected' : ''} ${
|
|
props.transparent ? 'action-button--transparent' : ''
|
|
}`}
|
|
style={props.style}
|
|
innerref={ref}
|
|
onClick={(e) => {
|
|
if (props.disabled) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
if (shouldConfirm) {
|
|
if (!confirmed) {
|
|
setDisplayLabel('SURE?');
|
|
setDisplayIcon('fa-check');
|
|
confirm(true);
|
|
e.preventDefault();
|
|
return;
|
|
} else {
|
|
confirm(false);
|
|
setDisplayLabel(props.label);
|
|
setDisplayIcon(props.icon);
|
|
}
|
|
}
|
|
if (action && (shouldConfirm ? confirmed : true)) {
|
|
action(e);
|
|
setLoading(true);
|
|
setSuccess(true);
|
|
e.preventDefault();
|
|
}
|
|
}}>
|
|
{icon && <i className={`fa ${getIcon()} ${(loading || props.loading) && indicateLoading && 'fa-spin'}`} />}
|
|
{label && (!icon || !short) && <span style={icon && {marginLeft: '5px'}}>{displayLabel}</span>}
|
|
</EffectDiv>
|
|
);
|
|
return props.tooltip ? <Tooltip content={props.tooltip}>{button}</Tooltip> : button;
|
|
};
|
|
|
|
export default ActionButton;
|