Skip to content
7 changes: 5 additions & 2 deletions skills/igniteui-blazor-components/references/form-controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,19 @@ Radios are grouped by being children of `IgbRadioGroup`, and the selected option
Mode="ColorPickerMode.Input" Format="ColorFormat.Rgb"
ShowAlpha="true" Swatches="Palette" />

<IgbColorPicker @bind-Value="Highlight" Label="Highlight" />

@code {
string? Background { get; set; } = "#875fc4";
string? Accent { get; set; }
System.Drawing.Color? Highlight { get; set; }
static readonly string[] Palette = ["#e91e63", "#3f51b5", "#009688"];
}
```

An HSV canvas with hue and alpha sliders, an editable color string, preset swatches, and the native EyeDropper where the browser has one. `Value` is a CSS color string (hex, `rgb(a)`, `hsl(a)`, or a named color); an empty or invalid string clears it. `Format` (`Hex | Rgb | Hsl`) changes only the notation, not the color, and `HideFormats` drops the format switcher. `Mode` is `Default` (trigger button) or `Input` (editable text field with a swatch prefix).
An HSV canvas with hue and alpha sliders, an editable color string, preset swatches, and the native EyeDropper where the browser has one. `IgbColorPicker<TValue>` takes `string` or `System.Drawing.Color`, either nullable; `@bind-Value` infers it, a picker without a value needs `TValue` set. As a `string`, `Value` is a CSS color string (hex, `rgb(a)`, `hsl(a)`, or a named color); an empty or invalid string clears it, and a cleared picker reports `""`. As a `Color`, `null` or `Color.Empty` clears it and a cleared picker reports the same; in `Hsl` format the color is rounded to whole-number hue, saturation and lightness. `Format` (`Hex | Rgb | Hsl`) changes only the notation, not the color, and `HideFormats` drops the format switcher. `Mode` is `Default` (trigger button) or `Input` (editable text field with a swatch prefix).

`Input` fires on every color change while `Change` fires on commit and drives `@bind-Value`; `Opening` / `Opened` / `Closing` / `Closed` track the picker surface. `Required`, `Disabled`, `Invalid` plus `CheckValidityAsync()` / `ReportValidityAsync()` / `SetCustomValidityAsync(message)` behave as on the other form controls.
`Input` fires on every color change while `Change` fires on commit and drives `@bind-Value`; both carry an `IgbColorPickerValueEventArgs<TValue>` whose `Detail` is the color as `TValue`. `Opening` / `Opened` / `Closing` / `Closed` track the picker surface. `Required`, `Disabled`, `Invalid` plus `CheckValidityAsync()` / `ReportValidityAsync()` / `SetCustomValidityAsync(message)` behave as on the other form controls.

## Binding and validation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Only pick a grid when the content is genuinely tabular. A list of records with r
| Radio options | `IgbRadioGroup` + `IgbRadio` | `@bind-Value` on the **group**; do not use `Name` to group |
| Slider / range slider | `IgbSlider` / `IgbRangeSlider` | range uses `Lower` / `Upper` |
| Star rating | `IgbRating` | |
| Color picker / color swatch input | `IgbColorPicker` | `@bind-Value` (CSS color string), `Mode` (`Default` trigger / `Input` field), `Format`, `ShowAlpha`, `Swatches` |
| Color picker / color swatch input | `IgbColorPicker` | `@bind-Value` (CSS color `string` or `System.Drawing.Color`), `Mode` (`Default` trigger / `Input` field), `Format`, `ShowAlpha`, `Swatches` |
| Primary action button | `IgbButton` | `Variant`: `Contained`/`Outlined`/`Flat`/`Fab` |
| Segmented / toggle control | `IgbButtonGroup` + `IgbToggleButton` | |
| Icon-only button | `IgbIconButton` | |
Expand Down
2 changes: 1 addition & 1 deletion src/components/Blazor/ColorFormat.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace IgniteUI.Blazor.Controls
{
/// <summary>
/// The notation an <see cref="IgbColorPicker"/> renders its string value in.
/// The notation an <see cref="IgbColorPicker{TValue}"/> renders its string value in.
/// </summary>
public enum ColorFormat
{
Expand Down
90 changes: 57 additions & 33 deletions src/components/Blazor/ColorPicker.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components;
using System.Drawing;

namespace IgniteUI.Blazor.Controls
{
Expand All @@ -9,8 +10,19 @@ namespace IgniteUI.Blazor.Controls
/// or a named CSS color. Supports pre-defined swatches and the native EyeDropper
/// API, where the browser provides one.
/// </summary>
public partial class IgbColorPicker : IgbBaseComboBox
/// <typeparam name="TValue"><c>string</c> or <see cref="Color"/>, either nullable.</typeparam>
public partial class IgbColorPicker<TValue> : IgbBaseComboBox
{
internal override Type GenericType => Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue);

/// <summary>Constructs a color picker for string or <see cref="Color"/> values.</summary>
public IgbColorPicker()
{
if (GenericType != typeof(string) && GenericType != typeof(Color))
{
throw new InvalidOperationException($"Unsupported {GetType()} type param '{GenericType}'.");
}
}
/// <inheritdoc />
internal override string RendererType { get { return "WebColorPicker"; } }

Expand Down Expand Up @@ -41,19 +53,35 @@ private protected override string DirectRenderElementName
}
}

private string? _value;
private TValue? _value;

private static bool ColorValuesEqual(TValue? left, TValue? right)
{
if (left is Color leftColor && right is Color rightColor)
{
// Color's own equality also compares names, so Color.Red would differ from red parsed back from the
// element. ToArgb alone cannot tell Color.Empty, which clears the element, from transparent black.
return leftColor.IsEmpty == rightColor.IsEmpty && leftColor.ToArgb() == rightColor.ToArgb();
}

return EqualityComparer<TValue?>.Default.Equals(left, right);
}

/// <summary>
/// The value of the component as a CSS color string. Accepts hex, rgb(a),
/// hsl(a) and named colors. An empty, whitespace-only or invalid string clears the value.
/// The selected color.
/// As a <c>string</c>, a CSS color: hex, rgb(a), hsl(a) or a named color. An empty or invalid string clears the
/// picker, which reports its color in the <see cref="Format"/> notation, or <c>""</c> when cleared.
/// As a <see cref="Color"/>, <c>null</c> or <see cref="Color.Empty"/> clears the picker, which reports the same
/// when cleared. In <see cref="ColorFormat.Hsl"/> the picker reports whole-number hue, saturation and lightness,
/// so the <see cref="Color"/> can be a unit or two per channel off the picked color.
/// </summary>
[Parameter]
public string? Value
public TValue? Value
{
get { return this._value; }
set
{
if (this._value != value || !IsPropDirty("Value"))
if (!ColorValuesEqual(this._value, value) || !IsPropDirty("Value"))
{
MarkPropDirty("Value");
}
Expand All @@ -65,19 +93,19 @@ public string? Value
/// <summary>
/// Returns the current value of the component.
/// </summary>
public async Task<string?> GetCurrentValueAsync()
public async Task<TValue?> GetCurrentValueAsync()
{
var iv = await InvokeMethod("p:Value", new object[] { }, new string[] { });
return ReturnToString(iv);
return ColorPickerColorConverter.FromCss<TValue>(ReturnToString(iv));
}

/// <summary>
/// Returns the current value of the component.
/// </summary>
public string? GetCurrentValue()
public TValue? GetCurrentValue()
{
var iv = InvokeMethodSync("p:Value", new object[] { }, new string[] { });
return ReturnToString(iv);
return ColorPickerColorConverter.FromCss<TValue>(ReturnToString(iv));
}
private string? _label;

Expand Down Expand Up @@ -329,18 +357,18 @@ public void SetCustomValidity(String message)
InvokeMethodSync("setCustomValidity", new object?[] { StringToString(message) }, new string[] { "String" });
}

private EventCallback<string?>? _valueChanged = null;
private EventCallback<TValue?>? _valueChanged = null;

/// <summary>
/// Emitted when the <see cref="Value"/> property changes.
/// Enables two-way binding through <c>@bind-Value</c>.
/// </summary>
[Parameter]
public EventCallback<string?> ValueChanged
public EventCallback<TValue?> ValueChanged
{
get
{
return this._valueChanged != null ? this._valueChanged.Value : EventCallback<string?>.Empty;
return this._valueChanged != null ? this._valueChanged.Value : EventCallback<TValue?>.Empty;
}
set
{
Expand Down Expand Up @@ -393,17 +421,17 @@ public string? ChangeScript
}
}

private EventCallback<IgbComponentValueChangedEventArgs>? _change = null;
private EventCallback<IgbColorPickerValueEventArgs<TValue?>>? _change = null;

/// <summary>
/// Emitted when the value of the component is committed.
/// </summary>
[Parameter]
public EventCallback<IgbComponentValueChangedEventArgs> Change
public EventCallback<IgbColorPickerValueEventArgs<TValue?>> Change
{
get
{
return this._change != null ? this._change.Value : EventCallback<IgbComponentValueChangedEventArgs>.Empty;
return this._change != null ? this._change.Value : EventCallback<IgbColorPickerValueEventArgs<TValue?>>.Empty;
}
set
{
Expand All @@ -412,12 +440,11 @@ public EventCallback<IgbComponentValueChangedEventArgs> Change
if (!value.EqualsCompat(_change))
{
_change = value;
this.SetHandler<IgbComponentValueChangedEventArgs>(this.RendererName, "Change", value, (args) =>
this.SetHandler<IgbColorPickerValueEventArgs<TValue?>>(this.RendererName, "Change", value, (args) =>
{
var newValueValue = default(string?);
var newValueValue = args.Detail;

{
newValueValue = (string?)(args.Detail);
if (UseDirectRender)
{
//TODO: maybe we should be doing this for everything. Need to make sure we don't infinity bounce though.
Expand All @@ -430,13 +457,10 @@ public EventCallback<IgbComponentValueChangedEventArgs> Change
OnPropertyPropagatedOut(RendererName, "Value");
}

if (!EventCallback<string?>.Empty.Equals(ValueChanged))
if (!EventCallback<TValue?>.Empty.Equals(ValueChanged))
{
var task = ValueChanged.InvokeAsync(newValueValue);
if (task.Exception != null)
{
throw task.Exception;
}
ObserveHandlerTask(task);
}

});
Expand All @@ -450,7 +474,7 @@ public EventCallback<IgbComponentValueChangedEventArgs> Change
else
{
_change = null;
this.SetHandler<IgbComponentValueChangedEventArgs>(this.RendererName, "Change", null);
this.SetHandler<IgbColorPickerValueEventArgs<TValue?>>(this.RendererName, "Change", null);
this.OnRefChanged("Change", null, null, true, false, (refName, oldValue, newValue) =>
{
this._changeRef = null;
Expand All @@ -461,9 +485,9 @@ public EventCallback<IgbComponentValueChangedEventArgs> Change
}
internal void EnsureChangeHandled()
{
if (EventCallback<IgbComponentValueChangedEventArgs>.Empty.Equals(this.Change))
if (EventCallback<IgbColorPickerValueEventArgs<TValue?>>.Empty.Equals(this.Change))
{
this.Change = new EventCallback<IgbComponentValueChangedEventArgs>(null, (Action<IgbComponentValueChangedEventArgs>)((e) => { }));
this.Change = new EventCallback<IgbColorPickerValueEventArgs<TValue?>>(null, (Action<IgbColorPickerValueEventArgs<TValue?>>)((e) => { }));
this._change = null;
}
}
Expand Down Expand Up @@ -501,17 +525,17 @@ public string? InputScript
}
}

private EventCallback<IgbComponentValueChangedEventArgs>? _input = null;
private EventCallback<IgbColorPickerValueEventArgs<TValue?>>? _input = null;

/// <summary>
/// Emitted when the value of the component is changed.
/// </summary>
[Parameter]
public EventCallback<IgbComponentValueChangedEventArgs> Input
public EventCallback<IgbColorPickerValueEventArgs<TValue?>> Input
{
get
{
return this._input != null ? this._input.Value : EventCallback<IgbComponentValueChangedEventArgs>.Empty;
return this._input != null ? this._input.Value : EventCallback<IgbColorPickerValueEventArgs<TValue?>>.Empty;
}
set
{
Expand All @@ -520,7 +544,7 @@ public EventCallback<IgbComponentValueChangedEventArgs> Input
if (!value.EqualsCompat(_input))
{
_input = value;
this.SetHandler<IgbComponentValueChangedEventArgs>(this.RendererName, "Input", value);
this.SetHandler<IgbColorPickerValueEventArgs<TValue?>>(this.RendererName, "Input", value);
this.OnRefChanged("Input", null, "event:::Input", true, false, (refName, oldValue, newValue) =>
{
this._inputRef = refName;
Expand All @@ -531,7 +555,7 @@ public EventCallback<IgbComponentValueChangedEventArgs> Input
else
{
_input = null;
this.SetHandler<IgbComponentValueChangedEventArgs>(this.RendererName, "Input", null);
this.SetHandler<IgbColorPickerValueEventArgs<TValue?>>(this.RendererName, "Input", null);
this.OnRefChanged("Input", null, null, true, false, (refName, oldValue, newValue) =>
{
this._inputRef = null;
Expand Down Expand Up @@ -838,7 +862,7 @@ internal override void SerializeCore(RendererSerializer ser)
base.SerializeCore(ser);

if (IsPropDirty("Value"))
{ ser.AddStringProp("value", this._value); }
{ ser.AddStringProp("value", ColorPickerColorConverter.ToCss(this._value)); }
if (IsPropDirty("Label"))
{ ser.AddStringProp("label", this._label); }
if (IsPropDirty("Name"))
Expand Down
2 changes: 1 addition & 1 deletion src/components/Blazor/ColorPickerMode.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace IgniteUI.Blazor.Controls
{
/// <summary>
/// How an <see cref="IgbColorPicker"/> presents the anchor that opens it.
/// How an <see cref="IgbColorPicker{TValue}"/> presents the anchor that opens it.
/// </summary>
public enum ColorPickerMode
{
Expand Down
2 changes: 1 addition & 1 deletion src/components/Blazor/ColorPickerModule.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace IgniteUI.Blazor.Controls
{
/// <summary>
/// Client resource module for <see cref="IgbColorPicker"/>.
/// Client resource module for <see cref="IgbColorPicker{TValue}"/>.
/// </summary>
/// <remarks>
/// Register explicitly on application startup by passing this type to <c>AddIgniteUIBlazor</c>.
Expand Down
66 changes: 66 additions & 0 deletions src/components/Blazor/ColorPickerValueEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace IgniteUI.Blazor.Controls
{
/// <summary>
/// Event arguments for <see cref="IgbColorPicker{TValue}"/> events that carry a color.
/// The meaning of <see cref="Detail"/> depends on the event that raises it.
/// </summary>
public partial class IgbColorPickerValueEventArgs<TValue> : BaseJsonSerializable
{
/// <inheritdoc />
// The element sends the same string payload as for IgbComponentValueChangedEventArgs; only the decoding differs.
internal override string RendererType { get { return "WebComponentValueChangedEventArgs"; } }

private TValue? _detail;

/// <summary>
/// The color carried by the event, in the picker's <typeparamref name="TValue"/>.
/// </summary>
public TValue? Detail
{
get { return this._detail; }
set
{
if (!EqualityComparer<TValue>.Default.Equals(this._detail, value) || !IsPropDirty("Detail"))
{
MarkPropDirty("Detail");
}
this._detail = value;

}
}

internal override void SerializeCore(RendererSerializer ser)
{
base.SerializeCore(ser);

if (IsPropDirty("Detail"))
{ ser.AddStringProp("detail", ColorPickerColorConverter.ToCss(this._detail)); }

}

/// <inheritdoc />
internal override void ToEventJson(IgbComponentBase control, Dictionary<string, object?> args)
{
base.ToEventJson(control, args);

if (IsPropDirty("Detail"))
{ args["detail"] = ColorPickerColorConverter.ToCss(this._detail); }

}

/// <inheritdoc />
internal override void FromEventJson(IgbComponentBase control, Dictionary<string, object?>? args)
{
base.FromEventJson(control, args);
this.SuppressParentNotify = true;

if (args != null && args.TryGetValue("detail", out var detailObj))
{
this.Detail = ColorPickerColorConverter.FromCss<TValue>(ReturnToString(detailObj));
}

this.SuppressParentNotify = false;
}

}
}
Loading