Skip to content

Commit

Permalink
Editor Action Bar - Mirror Editor Actions (#5428)
Browse files Browse the repository at this point in the history
  • Loading branch information
softwarenerd authored Nov 21, 2024
1 parent e168eb2 commit 59b5b0c
Show file tree
Hide file tree
Showing 31 changed files with 1,179 additions and 336 deletions.
3 changes: 3 additions & 0 deletions build/lib/stylelint/vscode-known-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,9 @@
"--test-bar-width",
"--widget-color",
"--text-link-decoration",
"--positronActionBar-background",
"--positronActionBar-border",
"--positronActionBar-height",
"--positron-data-grid-column-header-description-font-size",
"--positron-data-grid-column-header-sort-index-font-size",
"--positron-data-grid-column-header-sort-index-font-variant-numeric",
Expand Down
72 changes: 34 additions & 38 deletions src/vs/base/browser/ui/positronComponents/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
// CSS.
import 'vs/css!./button';


// React.
import * as React from 'react';
import { CSSProperties, forwardRef, KeyboardEvent, MouseEvent, PropsWithChildren, useImperativeHandle, useRef } from 'react'; // eslint-disable-line no-duplicate-imports
import { CSSProperties, forwardRef, KeyboardEvent, MouseEvent, PropsWithChildren, useImperativeHandle, useRef, useState } from 'react'; // eslint-disable-line no-duplicate-imports

// Other dependencies.
import { positronClassNames } from 'vs/base/common/positronUtilities';
import { IHoverManager } from 'vs/platform/positronActionBar/browser/positronActionBarState';
import { IHoverManager } from 'vs/platform/hover/browser/hoverManager';

/**
* MouseTrigger enumeration.
Expand Down Expand Up @@ -47,6 +46,8 @@ interface ButtonProps {
readonly mouseTrigger?: MouseTrigger;
readonly onBlur?: () => void;
readonly onFocus?: () => void;
readonly onMouseEnter?: () => void;
readonly onMouseLeave?: () => void;
readonly onPressed?: (e: KeyboardModifiers) => void;
}

Expand All @@ -62,11 +63,28 @@ export const Button = forwardRef<HTMLButtonElement, PropsWithChildren<ButtonProp
// Customize the ref handle that is exposed.
useImperativeHandle(ref, () => buttonRef.current, []);

// State hooks.
const [mouseInside, setMouseInside] = useState(false);

// Hover useEffect.
React.useEffect(() => {
// If the mouse is inside, show the hover. This has the effect of showing the hover when
// mouseInside is set to true and updating the hover when the tooltip changes.
if (mouseInside) {
props.hoverManager?.showHover(buttonRef.current, props.tooltip);
}
}, [mouseInside, props.hoverManager, props.tooltip]);

/**
* Sends the onPressed event.
* @param e The event that triggered the onPressed event.
*/
const sendOnPressed = (e: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>) => {
// Consume the event.
e.preventDefault();
e.stopPropagation();

// Hide the hover.
props.hoverManager?.hideHover();

// Raise the onPressed event if the button isn't disabled.
Expand Down Expand Up @@ -106,48 +124,26 @@ export const Button = forwardRef<HTMLButtonElement, PropsWithChildren<ButtonProp
* @param e A MouseEvent<HTMLDivElement> that describes a user interaction with the mouse.
*/
const mouseEnterHandler = (e: MouseEvent<HTMLButtonElement>) => {
// If there's a hover manager, see if there's a tooltip.
if (props.hoverManager) {
// Get the tooltip.
const tooltip = (() => {
if (!props.tooltip) {
// There isn't a tooltip.
return undefined;
} else if (typeof props.tooltip === 'string') {
// Return the string tooltip.
return props.tooltip;
} else {
// Return the dynamic tooltip.
return props.tooltip();
}
})();

// If there's a tooltip, show it.
if (tooltip) {
props.hoverManager.showHover({
content: tooltip,
target: buttonRef.current,
persistence: {
hideOnKeyDown: true,
hideOnHover: false
},
appearance: {
showPointer: true
}
}, false);
}
}
// Set the mouse inside state.
setMouseInside(true);

// Call the onMouseEnter callback.
props.onMouseEnter?.();
};

/**
* onMouseLeave event handler.
* @param e A MouseEvent<HTMLDivElement> that describes a user interaction with the mouse.
*/
const mouseLeaveHandler = (e: MouseEvent<HTMLButtonElement>) => {
// If there's a hover manager, hide hover.
if (props.hoverManager) {
props.hoverManager.hideHover();
}
// Clear the mouse inside state.
setMouseInside(false);

// Hide the hover.
props.hoverManager?.hideHover();

// Call the onMouseLeave callback.
props.onMouseLeave?.();
};

/**
Expand Down
2 changes: 1 addition & 1 deletion src/vs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1627,7 +1627,7 @@ var AMDLoader;
} else {
// Unit tests load client.js from 'http://localhost'. For example:
// http://localhost:51262/e9416c1769b269baf1f33978a0695be1/node_modules/react-dom/umd/react-dom.production.min.js/client.js
const reactDomClientLocalhost = /(http:\/\/localhost:[0-9]+\/[0-9A-Fa-f]+)(\/node_modules\/react-dom\/umd\/react-dom.production.min.js\/client.js)/;
const reactDomClientLocalhost = /^(http:\/\/localhost:[0-9]+\/[0-9A-Fa-f]+)(\/node_modules\/react-dom\/umd\/react-dom.production.min.js\/client.js)$/;
const result = paths[0].match(reactDomClientLocalhost);
if (result && result.length === 3) {
paths[0] = `${result[1]}/out/react-dom/client.js`;
Expand Down
21 changes: 21 additions & 0 deletions src/vs/platform/hover/browser/hoverManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

/**
* IHoverManager interface.
*/
export interface IHoverManager {
/**
* Shows a hover.
* @param target The target.
* @param content The content.
*/
showHover(target: HTMLElement, content?: string | (() => string | undefined)): void;

/**
* Hides the hover if it was visible.
*/
hideHover(): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

// CSS.
import 'vs/css!./actionBarActionButton';

// React.
import * as React from 'react';
import { useEffect, useRef, useState } from 'react'; // eslint-disable-line no-duplicate-imports

// Other dependencies.
import { IAction } from 'vs/base/common/actions';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { useStateRef } from 'vs/base/browser/ui/react/useStateRef';
import { MenuItemAction } from 'vs/platform/actions/common/actions';
import { IModifierKeyStatus, ModifierKeyEmitter } from 'vs/base/browser/dom';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { useRegisterWithActionBar } from 'vs/platform/positronActionBar/browser/useRegisterWithActionBar';
import { usePositronActionBarContext } from 'vs/platform/positronActionBar/browser/positronActionBarContext';
import { ActionBarButton, ActionBarButtonProps } from 'vs/platform/positronActionBar/browser/components/actionBarButton';
import { actionTooltip, toMenuActionItem } from 'vs/platform/positronActionBar/common/helpers';

/**
* Constants.
*/
const CODICON_ID = /^codicon codicon-(.+)$/;

/**
* Determines whether the alternative action should be used.
* @param accessibilityService The accessibility service.
* @param menuItemAction The menu item action.
* @param mouseOver Whether the mouse is over the action bar action button.
* @param modifierKeyStatus The modifier key status.
* @returns A value which indicates whether the alternative action should be used.
*/
const shouldUseAlternativeAction = (
accessibilityService: IAccessibilityService,
menuItemAction?: MenuItemAction,
mouseOver?: boolean,
modifierKeyStatus?: IModifierKeyStatus
) => {
// If a menu item action was not supplied, return false
if (!menuItemAction) {
return false;
}

// If there isn't an alt action, or there is and it's not enabled, return false
if (!menuItemAction.alt?.enabled) {
return false;
}

// If the modifier key status was not supplied, get it from the modifier key emitter.
if (!modifierKeyStatus) {
modifierKeyStatus = ModifierKeyEmitter.getInstance().keyStatus;
}

// If motion is not reduced and the alt key is pressed, return true.
if (!accessibilityService.isMotionReduced() && modifierKeyStatus.altKey) {
return true;
}

// If the mouse is over the action bar action button and the shift or alt key is pressed, return
// true.
if (mouseOver && (modifierKeyStatus.shiftKey || modifierKeyStatus.altKey)) {
return true;
}

// Do not use the alternative action.
return false;
};

/**
* ActionBarActionButtonProps interface.
*/
interface ActionBarActionButtonProps {
readonly action: IAction;
}

/**
* ActionBarCommandButton component.
* @param props An ActionBarCommandButtonProps that contains the component properties.
* @returns The rendered component.
*/
export const ActionBarActionButton = (props: ActionBarActionButtonProps) => {
// Context hooks.
const context = usePositronActionBarContext();

// Reference hooks.
const buttonRef = useRef<HTMLButtonElement>(undefined!);

// Menu action item.
const menuActionItem = toMenuActionItem(props.action);

// State hooks.
const [, setMouseInside, mouseInsideRef] = useStateRef(false);
const [useAlternativeAction, setUseAlternativeAction] = useState(
shouldUseAlternativeAction(context.accessibilityService, menuActionItem)
);

// Main use effect.
useEffect(() => {
// Create the disposable store for cleanup.
const disposableStore = new DisposableStore();

// Get the modifier key emitter and add the event listener to it.
const modifierKeyEmitter = ModifierKeyEmitter.getInstance();
disposableStore.add(modifierKeyEmitter.event(modifierKeyStatus => {
setUseAlternativeAction(shouldUseAlternativeAction(
context.accessibilityService,
menuActionItem,
mouseInsideRef.current,
modifierKeyStatus
));
}));

// Return the cleanup function that will dispose of the disposables.
return () => disposableStore.dispose();
}, [context.accessibilityService, menuActionItem, mouseInsideRef]);

// Participate in roving tabindex.
useRegisterWithActionBar([buttonRef]);

// Get the action we're going to render.
const action = menuActionItem &&
useAlternativeAction &&
menuActionItem.alt?.enabled ? menuActionItem.alt : props.action;

// Build the dynamic properties.
const dynamicProps = ((): ActionBarButtonProps => {
// Extract the icon ID from the action's class.
const iconIdResult = action.class?.match(CODICON_ID);
const iconId = iconIdResult?.length === 2 ? iconIdResult[1] : undefined;

// Return the properties.
return {
ariaLabel: action.label ?? action.tooltip,
iconId: iconId,
tooltip: actionTooltip(
context.contextKeyService,
context.keybindingService,
action,
!useAlternativeAction
),
disabled: !action.enabled,
onMouseEnter: () => setMouseInside(true),
onMouseLeave: () => setMouseInside(false),
onPressed: () =>
action.run()
};
})();

// Render.
return (
<ActionBarButton
ref={buttonRef}
{...dynamicProps}
/>
);
};
Loading

0 comments on commit 59b5b0c

Please sign in to comment.