Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion localization/l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@
"Message": "Message",
"Open in New Tab": "Open in New Tab",
"Showplan XML": "Showplan XML",
"Show Menu": "Show Menu",
"Show Menu (F3)": "Show Menu (F3)",
"Sort Ascending": "Sort Ascending",
"Sort Descending": "Sort Descending",
"Clear Sort": "Clear Sort",
Expand Down Expand Up @@ -409,6 +409,7 @@
"message": "{0} selected",
"comment": ["{0} is the number of selected rows"]
},
"Sort": "Sort",
"Add new column": "Add new column",
"Table": "Table",
"Save": "Save",
Expand Down
7 changes: 5 additions & 2 deletions localization/xliff/vscode-mssql.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -2935,8 +2935,8 @@
<trans-unit id="++CODE++8c6f0777a59f6d55efb68be5323e48061f19e0b2c036c9f34f3ea7655eeaead2">
<source xml:lang="en">Show MSSQL output</source>
</trans-unit>
<trans-unit id="++CODE++a16e132b133cc57c4d467bde193d78fe48f170a197d0581c2e6578162824902f">
<source xml:lang="en">Show Menu</source>
<trans-unit id="++CODE++1615831401345ad7af7c55f1b68f51dd051827cf4ab1e7f3864619ce377d6ef1">
<source xml:lang="en">Show Menu (F3)</source>
</trans-unit>
<trans-unit id="++CODE++169b9d739add25522ff9a236b54583826c27bd79c2dcb1a4efcc50143164f78c">
<source xml:lang="en">Show New Password</source>
Expand Down Expand Up @@ -3000,6 +3000,9 @@
<trans-unit id="++CODE++ab3134048412e796a4a423dc1963ca8ff12b535d546d314470407639777e0ac5">
<source xml:lang="en">Smart performance</source>
</trans-unit>
<trans-unit id="++CODE++bec69036aa27e7fab7d44cad3909477b76631c39ba46fd7841ea71aae7e5a735">
<source xml:lang="en">Sort</source>
</trans-unit>
<trans-unit id="++CODE++f09318d75d62b0f316bf6aa9aec677f5ee481f619c46c57d72dc9505e73fe48f">
<source xml:lang="en">Sort Ascending</source>
</trans-unit>
Expand Down
6 changes: 6 additions & 0 deletions src/reactviews/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
*--------------------------------------------------------------------------------------------*/

export const addNewMicrosoftAccount = "##_addNewMicrosoftAccount_##";

export const cmdAKeyboardShortcut = "⌘A";
export const ctrlAKeyboardShortcut = "Ctrl+A";
export const cmdCKeyboardShortcut = "⌘C";
export const ctrlCKeyboardShortcut = "Ctrl+C";
export const altShiftOKeyboardShortcut = "Shift+Alt+O";
75 changes: 38 additions & 37 deletions src/reactviews/common/icons/FLUENT_ICONS.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,60 @@
## Creating custom React Fluent icons

1. Clean up
Fluent icons are constructed with an array of paths, but without any `fill-rule` or `clip-rule` entries. You can use the free/OSS Inkscape to clean this up easily:
Fluent icons are constructed with an array of paths, but without any `fill-rule` or `clip-rule` entries. You can use the free/OSS Inkscape to clean this up easily:

1. Open your SVG
* File → Open… and load your SVG.
* Make sure your shape with fill-rule="evenodd" is visible.
1. Open your SVG

2. Select the path(s)
* Use the Select tool (S) and click the object.
* If it’s a compound path, you may need Object → Ungroup first.
- File → Open… and load your SVG.
- Make sure your shape with fill-rule="evenodd" is visible.

3. Convert the shape into geometry that respects the evenodd fill
* With the path selected, go to:
* Path → Break Apart (Shift+Ctrl+K)
* This splits the path into its component sub-paths.
* Inkscape interprets the evenodd rule at this step: “holes” become independent paths.
2. Select the path(s)

4. Subtract the holes (if present)
* Select the main outer shape, then the hole shapes.
* Use Path → Difference (Ctrl+-) to cut the holes out.
* Repeat until all inner holes are cut away.
* Now you have pure geometry with no reliance on fill-rule.
- Use the Select tool (S) and click the object.
- If it’s a compound path, you may need Object → Ungroup first.

3. Convert the shape into geometry that respects the evenodd fill

- With the path selected, go to:
- Path → Break Apart (Shift+Ctrl+K)
- This splits the path into its component sub-paths.
- Inkscape interprets the evenodd rule at this step: “holes” become independent paths.

4. Subtract the holes (if present)
- Select the main outer shape, then the hole shapes.
- Use Path → Difference (Ctrl+-) to cut the holes out.
- Repeat until all inner holes are cut away.
- Now you have pure geometry with no reliance on fill-rule.

2. Use this script to scale all the paths to your target size:

`npm install svgpath`
`npm install svgpath`

```js
// scaleSvg.js
```js
// scaleSvg.js

import svgpath from 'svgpath';
import svgpath from "svgpath";

const fabricPath = "M 42.400391..."; // replace with your path
const fabricPath = "M 42.400391..."; // replace with your path

const currentViewboxSize = 40; // replace with the viewbox from your existing SVG
const targetSize = 20; // replace with your target viewbox size
const currentViewboxSize = 40; // replace with the viewbox from your existing SVG
const targetSize = 20; // replace with your target viewbox size

const scale = targetSize / currentViewboxSize;
const scale = targetSize / currentViewboxSize;

const scaled = svgpath(fabricPath)
.scale(scale)
.toString();
const scaled = svgpath(fabricPath).scale(scale).toString();

console.log(scaled);
```
console.log(scaled);
```

`node scaleSvg.js`
`node scaleSvg.js`

3. Create the React icon:

```ts
// in this example, the target size is 20.
```ts
// in this example, the target size is 20.

import { createFluentIcon } from "@fluentui/react-icons";
const iconPath = "M19.2729..."; // paths scaled to target size 20, from previous step
export const CustomIcon20 = createFluentIcon("CustomIcon20", "20", [iconPath]);
```
import { createFluentIcon } from "@fluentui/react-icons";
const iconPath = "M19.2729..."; // paths scaled to target size 20, from previous step
export const CustomIcon20 = createFluentIcon("CustomIcon20", "20", [iconPath]);
```
50 changes: 49 additions & 1 deletion src/reactviews/common/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,54 @@ export enum KeyCode {
ArrowUp = "ArrowUp",
ArrowDown = "ArrowDown",
Space = "Space",
KeyC = "KeyC",
KeyA = "KeyA",
KeyB = "KeyB",
KeyC = "KeyC",
KeyD = "KeyD",
KeyE = "KeyE",
KeyF = "KeyF",
KeyG = "KeyG",
KeyH = "KeyH",
KeyI = "KeyI",
KeyJ = "KeyJ",
KeyK = "KeyK",
KeyL = "KeyL",
KeyM = "KeyM",
KeyN = "KeyN",
KeyO = "KeyO",
KeyP = "KeyP",
KeyQ = "KeyQ",
KeyR = "KeyR",
KeyS = "KeyS",
KeyT = "KeyT",
KeyU = "KeyU",
KeyV = "KeyV",
KeyW = "KeyW",
KeyX = "KeyX",
KeyY = "KeyY",
KeyZ = "KeyZ",
F1 = "F1",
F2 = "F2",
F3 = "F3",
F4 = "F4",
F5 = "F5",
F6 = "F6",
F7 = "F7",
F8 = "F8",
F9 = "F9",
F10 = "F10",
F11 = "F11",
F12 = "F12",
Digit1 = "Digit1",
Digit2 = "Digit2",
Digit3 = "Digit3",
Digit4 = "Digit4",
Digit5 = "Digit5",
Digit6 = "Digit6",
Digit7 = "Digit7",
Digit8 = "Digit8",
Digit9 = "Digit9",
Digit0 = "Digit0",
ContextMenu = "ContextMenu",
Tab = "Tab",
}
4 changes: 3 additions & 1 deletion src/reactviews/common/locConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ export class LocConstants {
message: l10n.t("Message"),
openResultInNewTab: l10n.t("Open in New Tab"),
showplanXML: l10n.t("Showplan XML"),
showMenu: l10n.t("Show Menu"),
showMenu: l10n.t("Show Menu (F3)"),
sortAscending: l10n.t("Sort Ascending"),
sortDescending: l10n.t("Sort Descending"),
clearSort: l10n.t("Clear Sort"),
Expand Down Expand Up @@ -517,6 +517,8 @@ export class LocConstants {
args: [count],
comment: ["{0} is the number of selected rows"],
}),
sort: l10n.t("Sort"),
filter: l10n.t("Filter"),
};
}

Expand Down
135 changes: 135 additions & 0 deletions src/reactviews/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,138 @@ export function isMac(): boolean {
export function isMetaKeyPressed(e: KeyboardEvent | MouseEvent | React.KeyboardEvent): boolean {
return isMac() ? e.metaKey : e.ctrlKey;
}

/**
* Selector string for focusable elements.
*/
const FOCUSABLE_SELECTOR = [
"a[href]",
"button",
"textarea",
'input:not([type="hidden"])',
"select",
"[tabindex]",
'[contenteditable="true"]',
]
.map((s) => `${s}:not([tabindex="-1"])`)
.join(",");

/**
* Check if an element is visible.
* @param el The element to check.
* @returns True if the element is visible, false otherwise.
*/
function isElementVisible(el: HTMLElement): boolean {
// Covers display:none/visibility:hidden/off-screen containers, etc.
const style = window.getComputedStyle(el);
if (style.visibility === "hidden" || style.display === "none") return false;

// offsetParent check misses fixed/absolute in some cases; getClientRects covers that
if (el.offsetParent === null && el.getClientRects().length === 0) return false;

return true;
}

/**
* Get all focusable elements within the given root.
* @param root The root element to limit the search within.
* @returns An array of focusable elements.
*/
function getFocusableElements(root: ParentNode = document): HTMLElement[] {
return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(
(el) => !el.hasAttribute("disabled") && isElementVisible(el),
);
}

/**
* Get the adjacent focusable element in the given direction.
* @param currentElement The current focused element.
* @param step The step direction: 1 for next, -1 for previous.
* @param root The root element to limit the search within.
* @returns The adjacent focusable element, or null if none found.
*/
function getAdjacentFocusableElement(
currentElement: HTMLElement,
step: 1 | -1,
root: ParentNode = document,
): HTMLElement | null {
const focusable = getFocusableElements(root);
if (focusable.length === 0) return null;

const idx = focusable.indexOf(currentElement);
if (idx === -1) return null;

const nextIdx = (idx + step + focusable.length) % focusable.length;
return focusable[nextIdx] ?? null;
}

/**
* Get the next focusable element.
* @param currentElement The current focused element.
* @param root The root element to limit the search within. If not provided, document is used.
* @returns The next focusable element, or null if none found.
*/
export function getNextFocusableElement(
currentElement: HTMLElement,
root?: ParentNode,
): HTMLElement | null {
return getAdjacentFocusableElement(currentElement, 1, root ?? document);
}

/**
* Get the previous focusable element.
* @param currentElement The current focused element.
* @param root The root element to limit the search within. If not provided, document is used.
* @returns The previous focusable element, or null if none found.
*/
export function getPreviousFocusableElement(
currentElement: HTMLElement,
root?: ParentNode,
): HTMLElement | null {
return getAdjacentFocusableElement(currentElement, -1, root ?? document);
}

/**
* Get the next focusable element outside the given container.
* @param container The container element to check against.
* @returns The next focusable element outside the container, or null if none found.
*/
export function getNextFocusableElementOutside(container: HTMLElement): HTMLElement | null {
const focusables = getFocusableElements();
const active = document.activeElement as HTMLElement | null;
if (!active) return null;

const currentIndex = focusables.findIndex((el) => el === active && container.contains(el));
if (currentIndex === -1) return null;

for (let i = currentIndex + 1; i < focusables.length; i++) {
const el = focusables[i];
if (!container.contains(el)) {
el.focus();
return el;
}
}
return null; // no next element outside the container
}

/**
* Get the previous focusable element outside the given container.
* @param container The container element to check against.
* @returns The previous focusable element outside the container, or null if none found.
*/
export function getPreviousFocusableElementOutside(container: HTMLElement): HTMLElement | null {
const focusables = getFocusableElements();
const active = document.activeElement as HTMLElement | null;
if (!active) return null;

const currentIndex = focusables.findIndex((el) => el === active && container.contains(el));
if (currentIndex === -1) return null;
for (let i = currentIndex - 1; i >= 0; i--) {
const el = focusables[i];
if (!container.contains(el)) {
el.focus();
return el;
}
}
return null; // no previous element outside the container
}
9 changes: 4 additions & 5 deletions src/reactviews/pages/QueryResult/commandBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Button, makeStyles, Tooltip } from "@fluentui/react-components";
import { Button, makeStyles, Toolbar, Tooltip } from "@fluentui/react-components";
import { useContext, useState } from "react";
import { QueryResultCommandsContext } from "./queryResultStateProvider";
import { useVscodeWebview2 } from "../../common/vscodeWebviewProvider2";
Expand All @@ -26,8 +26,7 @@ import {

const useStyles = makeStyles({
commandBar: {
display: "flex",
flexDirection: "column" /* Align buttons vertically */,
width: "16px",
},
buttonImg: {
display: "block",
Expand Down Expand Up @@ -109,7 +108,7 @@ const CommandBar = (props: CommandBarProps) => {
}

return (
<div className={classes.commandBar}>
<Toolbar vertical className={classes.commandBar}>
{/* View Mode Toggle */}
<Tooltip
content={
Expand Down Expand Up @@ -203,7 +202,7 @@ const CommandBar = (props: CommandBarProps) => {
title={locConstants.queryResult.saveAsInsert}
/>
</Tooltip>
</div>
</Toolbar>
);
};

Expand Down
Loading