Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d0281c3
Add ColorPicker component with styling and functionality
dvoituron Apr 12, 2026
07caf2c
Refactor ColorPicker component: update structure, add orientation sup…
dvoituron Apr 12, 2026
3e9d349
Add ColorPickerView enum and implement view selection in ColorPicker …
dvoituron Apr 12, 2026
2032d15
Implement Color Wheel view in ColorPicker component with SVG renderin…
dvoituron Apr 12, 2026
7438b8a
Remove ColorWheel SVG rendering and associated styles from ColorPicke…
dvoituron Apr 13, 2026
c7df38b
Add WheelColors list to FluentColorPicker component for enhanced colo…
dvoituron Apr 13, 2026
05e1923
Implement Color Wheel rendering in FluentColorPicker with interactive…
dvoituron Apr 13, 2026
ae8a875
Merge branch 'dev-v5' into users/dvoituron/dev-v5/color-picker
dvoituron Apr 17, 2026
b42d14b
Implement HSV color picker with interactive UI and JavaScript integra…
dvoituron Apr 17, 2026
1116959
Refactor FluentColorPicker to enhance structure and improve CSS speci…
dvoituron Apr 17, 2026
2355266
Update FluentColorPicker CSS to use min-width and min-height for HSV …
dvoituron Apr 17, 2026
6848c8c
Merge remote-tracking branch 'origin/dev-v5' into users/dvoituron/dev…
dvoituron Apr 17, 2026
b728a26
Enhance FluentColorPicker with improved CSS for SwatchPalette view an…
dvoituron Apr 17, 2026
fbca3b7
Add color indicator for Color Wheel view and update CSS styles
dvoituron Apr 17, 2026
2ae2280
Refactor FluentColorPicker styles for SwatchPalette orientation and e…
dvoituron Apr 17, 2026
2d4014a
Add JSRuntime extension method for safe JavaScript invocation in Colo…
dvoituron Apr 17, 2026
9c2c50d
Refactor FluentColorPicker to support custom color palettes and enhan…
dvoituron Apr 17, 2026
0955fa8
Enhance FluentColorPicker parameter handling with palette validation …
dvoituron Apr 17, 2026
aff9996
Add ColorHelper utility and FluentColorPicker enhancements for closes…
dvoituron Apr 17, 2026
19960a4
Refactor FluentColorPicker example to improve layout and remove unuse…
dvoituron Apr 17, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<FluentStack Wrap="true"
HorizontalGap="12px">
<FluentColorPicker Orientation="Orientation.Horizontal"
View="ColorPickerView.SwatchPalette"
@bind-SelectedColor="SelectedColor" />

<FluentColorPicker View="ColorPickerView.ColorWheel"
@bind-SelectedColor="SelectedColor" />

<FluentColorPicker View="ColorPickerView.HsvSquare"
@bind-SelectedColor="SelectedColor" />
</FluentStack>

<div>
<FluentText>Selected Color: @SelectedColor</FluentText>
</div>

@code
{
string SelectedColor = "#FF0000";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
title: ColorPicker
route: /ColorPicker
icon: TooltipQuote
---

# ColorPicker

XXX

## Example

{{ FluentColorPickerDefault }}

## API FluentColorPicker

{{ API Type=FluentColorPicker }}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<FluentStack>
<div>
<FluentButton Id="LeftButton" OnClick="@(e => LeftOpened = !LeftOpened)">Open Popover</FluentButton>
<FluentPopover AnchorId="LeftButton" @bind-Opened="@LeftOpened">
<FluentButton Id="LeftButton"
OnClick="@(e => LeftOpened = !LeftOpened)">Open Popover</FluentButton>
<FluentPopover AnchorId="LeftButton"
@bind-Opened="@LeftOpened">
<p>Example content for the</p>
<p>Popover component</p>
</FluentPopover>
Expand All @@ -10,8 +12,10 @@
<FluentSpacer />

<div>
<FluentButton Id="RightButton" OnClick="@(e => RightOpened = !RightOpened)">Open Popover</FluentButton>
<FluentPopover AnchorId="RightButton" @bind-Opened="@RightOpened">
<FluentButton Id="RightButton"
OnClick="@(e => RightOpened = !RightOpened)">Open Popover</FluentButton>
<FluentPopover AnchorId="RightButton"
@bind-Opened="@RightOpened">
<p>Example content for the</p>
<p>Popover component</p>
</FluentPopover>
Expand Down
239 changes: 239 additions & 0 deletions src/Core.Scripts/src/Components/ColorPicker/FluentColorPicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { DotNet } from "../../d-ts/Microsoft.JSInterop";

interface HsvState {
hue: number;
saturation: number;
value: number;
draggingSquare: boolean;
draggingHue: boolean;
dotNetHelper: DotNet.DotNetObject;
square: HTMLElement;
hueBar: HTMLElement;
squareIndicator: HTMLElement;
hueIndicator: HTMLElement;
mouseMoveHandler: (e: MouseEvent) => void;
mouseUpHandler: () => void;
touchMoveHandler: (e: TouchEvent) => void;
touchEndHandler: () => void;
}

const states = new Map<string, HsvState>();

export namespace Microsoft.FluentUI.Blazor.Components.ColorPicker {

/**
* Initializes the HSV color picker component.
* @param dotNetHelper The .NET object reference for callbacks.
* @param id The element ID of the color picker container.
* @param initialHue Initial hue value (0-360).
* @param initialSaturation Initial saturation value (0-1).
* @param initialValue Initial brightness value (0-1).
*/
export function Initialize(
dotNetHelper: DotNet.DotNetObject,
id: string,
initialHue: number,
initialSaturation: number,
initialValue: number
): void {

const container = document.getElementById(id);
if (!container) {
return;
}

const square = container.querySelector('[part="canvas"]') as HTMLElement;
const hueBar = container.querySelector('[part="hue-bar"]') as HTMLElement;
const squareIndicator = container.querySelector('[part="indicator"]') as HTMLElement;
const hueIndicator = container.querySelector('[part="hue-indicator"]') as HTMLElement;

if (!square || !hueBar || !squareIndicator || !hueIndicator) {
return;
}

const onSquareMove = (clientX: number, clientY: number): void => {
const state = states.get(id);
if (!state) {
return;
}

const rect = state.square.getBoundingClientRect();
const s = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const v = Math.max(0, Math.min(1, 1 - (clientY - rect.top) / rect.height));

state.saturation = s;
state.value = v;

state.squareIndicator.style.left = `${s * 100}%`;
state.squareIndicator.style.top = `${(1 - v) * 100}%`;

const hex = hsvToHex(state.hue, s, v);
state.dotNetHelper.invokeMethodAsync('FluentColorPicker.ColorChangedAsync', hex);
};

const onHueMove = (clientY: number): void => {
const state = states.get(id);
if (!state) {
return;
}

const rect = state.hueBar.getBoundingClientRect();
const t = Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));
const hue = t * 360;

state.hue = hue;
state.square.style.backgroundColor = `hsl(${hue}, 100%, 50%)`;
state.hueIndicator.style.top = `${t * 100}%`;

const hex = hsvToHex(hue, state.saturation, state.value);
state.dotNetHelper.invokeMethodAsync('FluentColorPicker.ColorChangedAsync', hex);
};

const mouseMoveHandler = (e: MouseEvent): void => {
const state = states.get(id);
if (!state) {
return;
}

if (state.draggingSquare) {
onSquareMove(e.clientX, e.clientY);
}
if (state.draggingHue) {
onHueMove(e.clientY);
}
};

const mouseUpHandler = (): void => {
const state = states.get(id);
if (state) {
state.draggingSquare = false;
state.draggingHue = false;
}
};

const touchMoveHandler = (e: TouchEvent): void => {
const state = states.get(id);
if (!state) {
return;
}

if (e.touches.length < 1) {
return;
}

if (e.cancelable) {
e.preventDefault();
}

const touch = e.touches[0];

if (state.draggingSquare) {
onSquareMove(touch.clientX, touch.clientY);
}
if (state.draggingHue) {
onHueMove(touch.clientY);
}
};

const touchEndHandler = (): void => {
const state = states.get(id);
if (state) {
state.draggingSquare = false;
state.draggingHue = false;
}
};

const state: HsvState = {
hue: initialHue,
saturation: initialSaturation,
value: initialValue,
draggingSquare: false,
draggingHue: false,
dotNetHelper,
square,
hueBar,
squareIndicator,
hueIndicator,
mouseMoveHandler,
mouseUpHandler,
touchMoveHandler,
touchEndHandler,
};

states.set(id, state);

// Square mouse events
square.addEventListener('mousedown', (e: MouseEvent) => {
const s = states.get(id);
if (s) {
s.draggingSquare = true;
onSquareMove(e.clientX, e.clientY);
}
});

// Hue bar mouse events
hueBar.addEventListener('mousedown', (e: MouseEvent) => {
const s = states.get(id);
if (s) {
s.draggingHue = true;
onHueMove(e.clientY);
}
});

// Square touch events
square.addEventListener('touchstart', (e: TouchEvent) => {
const s = states.get(id);
if (s && e.touches.length > 0) {
s.draggingSquare = true;
onSquareMove(e.touches[0].clientX, e.touches[0].clientY);
}
}, { passive: true });

// Hue bar touch events
hueBar.addEventListener('touchstart', (e: TouchEvent) => {
const s = states.get(id);
if (s && e.touches.length > 0) {
s.draggingHue = true;
onHueMove(e.touches[0].clientY);
}
}, { passive: true });

// Global document events for dragging
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
document.addEventListener('touchmove', touchMoveHandler, { passive: false });
document.addEventListener('touchend', touchEndHandler);
}

/**
* Disposes the HSV color picker state and removes event listeners.
* @param id The element ID of the color picker container.
*/
export function Dispose(id: string): void {
const state = states.get(id);
if (state) {
document.removeEventListener('mousemove', state.mouseMoveHandler);
document.removeEventListener('mouseup', state.mouseUpHandler);
document.removeEventListener('touchmove', state.touchMoveHandler);
document.removeEventListener('touchend', state.touchEndHandler);
states.delete(id);
}
}

function hsvToHex(h: number, s: number, v: number): string {
const c = v * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = v - c;
let r: number, g: number, b: number;

if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }

const toHex = (n: number): string => Math.round((n + m) * 255).toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
}
}
2 changes: 2 additions & 0 deletions src/Core.Scripts/src/ExportedMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Microsoft as FluentTextInput } from './Components/TextInput/TextInput';
import { Microsoft as FluentOverlayFile } from './Components/Overlay/FluentOverlay';
import { Microsoft as FluentListBoxContainerFile } from './Components/List/ListBoxContainer';
import { Microsoft as FluentAutocompleteFile } from './Components/List/FluentAutocomplete';
import { Microsoft as FluentColorPickerFile } from './Components/ColorPicker/FluentColorPicker';

export namespace Microsoft.FluentUI.Blazor.ExportedMethods {

Expand Down Expand Up @@ -42,6 +43,7 @@ export namespace Microsoft.FluentUI.Blazor.ExportedMethods {
(window as any).Microsoft.FluentUI.Blazor.Components.Overlay = FluentOverlayFile.FluentUI.Blazor.Components.Overlay;
(window as any).Microsoft.FluentUI.Blazor.Components.ListBoxContainer = FluentListBoxContainerFile.FluentUI.Blazor.Components.ListBoxContainer;
(window as any).Microsoft.FluentUI.Blazor.Components.Autocomplete = FluentAutocompleteFile.FluentUI.Blazor.Components.Autocomplete;
(window as any).Microsoft.FluentUI.Blazor.Components.ColorPicker = FluentColorPickerFile.FluentUI.Blazor.Components.ColorPicker;

// [^^^ Add your other exported methods before this line ^^^]
}
Expand Down
Loading
Loading