Skip to content

Commit 5d95aee

Browse files
committed
feat: add radio and checkbox group components for modals
Introduces DiscordRadioGroupComponent, DiscordCheckboxGroupComponent, and DiscordCheckboxComponent for modal-only usage, along with their option classes. Updates validation logic to restrict these components to modals, extends serialization and deserialization support, and adds new ComponentType enum values. Also adds builder methods to DiscordLabelComponent for attaching these new components.
1 parent 24acb2f commit 5d95aee

11 files changed

+468
-3
lines changed

DisCatSharp/Entities/Components/DiscordLabelComponent.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Generic;
2+
13
using DisCatSharp.Enums;
24
using DisCatSharp.Net.Serialization;
35

@@ -75,6 +77,36 @@ public DiscordLabelComponent WithFileUploadComponent(DiscordFileUploadComponent
7577
return this;
7678
}
7779

80+
/// <summary>
81+
/// Sets the radio group component for the label.
82+
/// </summary>
83+
/// <param name="component">The radio group component to attach to the label.</param>
84+
public DiscordLabelComponent WithRadioGroupComponent(DiscordRadioGroupComponent component)
85+
{
86+
this.Component = component;
87+
return this;
88+
}
89+
90+
/// <summary>
91+
/// Sets the checkbox group component for the label.
92+
/// </summary>
93+
/// <param name="component">The checkbox group component to attach to the label.</param>
94+
public DiscordLabelComponent WithCheckboxGroupComponent(DiscordCheckboxGroupComponent component)
95+
{
96+
this.Component = component;
97+
return this;
98+
}
99+
100+
/// <summary>
101+
/// Sets the checkbox component for the label.
102+
/// </summary>
103+
/// <param name="component">The checkbox component to attach to the label.</param>
104+
public DiscordLabelComponent WithCheckboxComponent(DiscordCheckboxComponent component)
105+
{
106+
this.Component = component;
107+
return this;
108+
}
109+
78110
/// <summary>
79111
/// The label.
80112
/// </summary>
@@ -100,6 +132,13 @@ public DiscordLabelComponent WithFileUploadComponent(DiscordFileUploadComponent
100132
public ComponentType SubComponentType
101133
=> (this.Component as DiscordComponent).Type;
102134

135+
/// <inheritdoc />
136+
public override IEnumerable<DiscordComponent> GetChildren()
137+
{
138+
if (this.Component is DiscordComponent component)
139+
yield return component;
140+
}
141+
103142
/// <summary>
104143
/// Assigns a unique id to this component.
105144
/// </summary>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System;
2+
3+
using DisCatSharp.Enums;
4+
5+
using Newtonsoft.Json;
6+
7+
namespace DisCatSharp.Entities;
8+
9+
/// <summary>
10+
/// Represents a single checkbox component. Modal-only.
11+
/// </summary>
12+
public sealed class DiscordCheckboxComponent : DiscordComponent, ILabelComponent
13+
{
14+
/// <summary>
15+
/// Creates a new empty checkbox component.
16+
/// </summary>
17+
internal DiscordCheckboxComponent()
18+
{
19+
this.Type = ComponentType.Checkbox;
20+
}
21+
22+
/// <summary>
23+
/// Creates a new checkbox component with the provided options.
24+
/// </summary>
25+
/// <param name="customId">The custom id for this component.</param>
26+
/// <param name="isDefault">Whether the checkbox is checked by default.</param>
27+
public DiscordCheckboxComponent(string? customId = null, bool? isDefault = null)
28+
: this()
29+
{
30+
this.CustomId = customId ?? Guid.NewGuid().ToString();
31+
this.Default = isDefault;
32+
}
33+
34+
/// <summary>
35+
/// The custom id for this component.
36+
/// </summary>
37+
[JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)]
38+
public override string? CustomId { get; internal set; } = Guid.NewGuid().ToString();
39+
40+
/// <summary>
41+
/// Whether the checkbox is checked by default.
42+
/// </summary>
43+
[JsonProperty("default", NullValueHandling = NullValueHandling.Ignore)]
44+
public bool? Default { get; internal set; }
45+
46+
/// <summary>
47+
/// The submitted value. Present on modal submit interactions.
48+
/// </summary>
49+
[JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)]
50+
public bool? Value { get; internal set; }
51+
52+
/// <summary>
53+
/// Assigns a unique id to this component.
54+
/// </summary>
55+
/// <param name="id">The id to assign.</param>
56+
public DiscordCheckboxComponent WithId(int id)
57+
{
58+
this.Id = id;
59+
return this;
60+
}
61+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
using DisCatSharp.Enums;
6+
7+
using Newtonsoft.Json;
8+
9+
namespace DisCatSharp.Entities;
10+
11+
/// <summary>
12+
/// Represents a checkbox group component. Modal-only.
13+
/// </summary>
14+
public sealed class DiscordCheckboxGroupComponent : DiscordComponent, ILabelComponent
15+
{
16+
/// <summary>
17+
/// Creates a new empty checkbox group component.
18+
/// </summary>
19+
internal DiscordCheckboxGroupComponent()
20+
{
21+
this.Type = ComponentType.CheckboxGroup;
22+
}
23+
24+
/// <summary>
25+
/// Creates a new checkbox group component with the provided options.
26+
/// </summary>
27+
/// <param name="options">The selectable options. Must contain between 1 and 10 entries.</param>
28+
/// <param name="customId">The custom id for this component.</param>
29+
/// <param name="minValues">The minimum number of selections. Defaults to 1.</param>
30+
/// <param name="maxValues">The maximum number of selections. Defaults to the number of options.</param>
31+
/// <param name="required">Whether a selection is required.</param>
32+
public DiscordCheckboxGroupComponent(IEnumerable<DiscordCheckboxGroupComponentOption> options, string? customId = null, int? minValues = null, int? maxValues = null, bool? required = null)
33+
: this()
34+
{
35+
ArgumentNullException.ThrowIfNull(options);
36+
var optionList = options.ToList();
37+
if (optionList.Count is < 1 or > 10)
38+
throw new ArgumentException("Checkbox groups must include between 1 and 10 options.");
39+
40+
var minimum = minValues ?? 1;
41+
var maximum = maxValues ?? optionList.Count;
42+
43+
if (minimum is < 0 or > 10)
44+
throw new ArgumentException("Minimum values must be between 0 and 10.", nameof(minValues));
45+
if (maximum is < 1 or > 10)
46+
throw new ArgumentException("Maximum values must be between 1 and 10.", nameof(maxValues));
47+
if (minimum > maximum)
48+
throw new ArgumentException("Minimum values cannot exceed maximum values.");
49+
if (maximum > optionList.Count)
50+
throw new ArgumentException("Maximum values cannot exceed the number of options.");
51+
52+
this.CustomId = customId ?? Guid.NewGuid().ToString();
53+
this.Options = optionList;
54+
this.MinimumValues = minimum;
55+
this.MaximumValues = maximum;
56+
this.Required = required;
57+
}
58+
59+
/// <summary>
60+
/// The custom id for this component.
61+
/// </summary>
62+
[JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)]
63+
public override string? CustomId { get; internal set; } = Guid.NewGuid().ToString();
64+
65+
/// <summary>
66+
/// The available options.
67+
/// </summary>
68+
[JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
69+
public IReadOnlyList<DiscordCheckboxGroupComponentOption> Options { get; internal set; } = Array.Empty<DiscordCheckboxGroupComponentOption>();
70+
71+
/// <summary>
72+
/// The minimum number of selections.
73+
/// </summary>
74+
[JsonProperty("min_values", NullValueHandling = NullValueHandling.Ignore)]
75+
public int? MinimumValues { get; internal set; } = 1;
76+
77+
/// <summary>
78+
/// The maximum number of selections.
79+
/// </summary>
80+
[JsonProperty("max_values", NullValueHandling = NullValueHandling.Ignore)]
81+
public int? MaximumValues { get; internal set; }
82+
83+
/// <summary>
84+
/// Whether the component requires a selection.
85+
/// </summary>
86+
[JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)]
87+
public bool? Required { get; internal set; }
88+
89+
/// <summary>
90+
/// The submitted values. Present on modal submit interactions.
91+
/// </summary>
92+
[JsonProperty("values", NullValueHandling = NullValueHandling.Ignore)]
93+
public string[]? Values { get; internal set; }
94+
95+
/// <summary>
96+
/// Assigns a unique id to this component.
97+
/// </summary>
98+
/// <param name="id">The id to assign.</param>
99+
public DiscordCheckboxGroupComponent WithId(int id)
100+
{
101+
this.Id = id;
102+
return this;
103+
}
104+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System;
2+
3+
using Newtonsoft.Json;
4+
5+
namespace DisCatSharp.Entities;
6+
7+
/// <summary>
8+
/// Represents an option within a checkbox group component.
9+
/// </summary>
10+
public sealed class DiscordCheckboxGroupComponentOption : ObservableApiObject
11+
{
12+
/// <summary>
13+
/// Creates a new checkbox group option.
14+
/// </summary>
15+
/// <param name="label">The display label. Max 100 characters.</param>
16+
/// <param name="value">The option value. Max 100 characters.</param>
17+
/// <param name="description">An optional description. Max 100 characters.</param>
18+
/// <param name="isDefault">Whether this option should be selected by default.</param>
19+
public DiscordCheckboxGroupComponentOption(string label, string value, string? description = null, bool isDefault = false)
20+
{
21+
if (string.IsNullOrWhiteSpace(label))
22+
throw new ArgumentException("Label must be provided.", nameof(label));
23+
if (string.IsNullOrWhiteSpace(value))
24+
throw new ArgumentException("Value must be provided.", nameof(value));
25+
if (label.Length > 100)
26+
throw new ArgumentException("Option label cannot exceed 100 characters.", nameof(label));
27+
if (value.Length > 100)
28+
throw new ArgumentException("Option value cannot exceed 100 characters.", nameof(value));
29+
if (description is { Length: > 100 })
30+
throw new ArgumentException("Option description cannot exceed 100 characters.", nameof(description));
31+
32+
this.Label = label;
33+
this.Value = value;
34+
this.Description = description;
35+
this.Default = isDefault;
36+
}
37+
38+
/// <summary>
39+
/// The display label.
40+
/// </summary>
41+
[JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)]
42+
public string Label { get; internal set; }
43+
44+
/// <summary>
45+
/// The underlying value returned on submit.
46+
/// </summary>
47+
[JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)]
48+
public string Value { get; internal set; }
49+
50+
/// <summary>
51+
/// Optional helper text.
52+
/// </summary>
53+
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)]
54+
public string? Description { get; internal set; }
55+
56+
/// <summary>
57+
/// Whether this option is pre-selected.
58+
/// </summary>
59+
[JsonProperty("default", NullValueHandling = NullValueHandling.Ignore)]
60+
public bool Default { get; internal set; }
61+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
using DisCatSharp.Enums;
6+
7+
using Newtonsoft.Json;
8+
9+
namespace DisCatSharp.Entities;
10+
11+
/// <summary>
12+
/// Represents a radio group component. Modal-only.
13+
/// </summary>
14+
public sealed class DiscordRadioGroupComponent : DiscordComponent, ILabelComponent
15+
{
16+
/// <summary>
17+
/// Creates a new empty radio group component.
18+
/// </summary>
19+
internal DiscordRadioGroupComponent()
20+
{
21+
this.Type = ComponentType.RadioGroup;
22+
}
23+
24+
/// <summary>
25+
/// Creates a new radio group component with the provided options.
26+
/// </summary>
27+
/// <param name="options">The selectable options. Must contain between 2 and 10 entries.</param>
28+
/// <param name="customId">The custom id for this component.</param>
29+
/// <param name="required">Whether a selection is required.</param>
30+
public DiscordRadioGroupComponent(IEnumerable<DiscordRadioGroupComponentOption> options, string? customId = null, bool? required = null)
31+
: this()
32+
{
33+
ArgumentNullException.ThrowIfNull(options);
34+
var optionList = options.ToList();
35+
if (optionList.Count is < 2 or > 10)
36+
throw new ArgumentException("Radio groups must include between 2 and 10 options.");
37+
if (optionList.Count(x => x.Default) > 1)
38+
throw new ArgumentException("Only one radio option can be marked as default.");
39+
40+
this.CustomId = customId ?? Guid.NewGuid().ToString();
41+
this.Options = optionList;
42+
this.Required = required;
43+
}
44+
45+
/// <summary>
46+
/// The custom id for this component.
47+
/// </summary>
48+
[JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)]
49+
public override string? CustomId { get; internal set; } = Guid.NewGuid().ToString();
50+
51+
/// <summary>
52+
/// The available options.
53+
/// </summary>
54+
[JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
55+
public IReadOnlyList<DiscordRadioGroupComponentOption> Options { get; internal set; } = Array.Empty<DiscordRadioGroupComponentOption>();
56+
57+
/// <summary>
58+
/// Whether the component requires a selection.
59+
/// </summary>
60+
[JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)]
61+
public bool? Required { get; internal set; }
62+
63+
/// <summary>
64+
/// The submitted values. Present on modal submit interactions.
65+
/// </summary>
66+
[JsonProperty("values", NullValueHandling = NullValueHandling.Ignore)]
67+
public string[]? Values { get; internal set; }
68+
69+
/// <summary>
70+
/// Convenience accessor for the first submitted value.
71+
/// </summary>
72+
[JsonIgnore]
73+
public string? SelectedValue
74+
=> this.Values?.FirstOrDefault();
75+
76+
/// <summary>
77+
/// Assigns a unique id to this component.
78+
/// </summary>
79+
/// <param name="id">The id to assign.</param>
80+
public DiscordRadioGroupComponent WithId(int id)
81+
{
82+
this.Id = id;
83+
return this;
84+
}
85+
}

0 commit comments

Comments
 (0)