Files
jwinters01andGitHub ad616de862 chore: fix Rules-of-React violations for React Compiler readiness (#624)
* 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]>
2026-06-30 11:51:17 +02:00

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;