feat!: label component and selects in modal (#11081)

BREAKING CHANGE: TextInputComponentData no longer accepts label
BREAKING CHANGE: ActionRow and ActionRowData no longer accept TextInput
BREAKING CHANGE: `ModalSubmitInteraction#transformComponent` is now private and no longer exposed publicly

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
This commit is contained in:
Naiyar
2025-09-11 02:04:57 +05:30
committed by GitHub
parent f1bcff46b6
commit 4ec03ae517
8 changed files with 234 additions and 57 deletions

View File

@@ -180,6 +180,7 @@ exports.InteractionCallbackResponse =
exports.InteractionCollector = require('./structures/InteractionCollector.js').InteractionCollector; exports.InteractionCollector = require('./structures/InteractionCollector.js').InteractionCollector;
exports.InteractionWebhook = require('./structures/InteractionWebhook.js').InteractionWebhook; exports.InteractionWebhook = require('./structures/InteractionWebhook.js').InteractionWebhook;
exports.InviteGuild = require('./structures/InviteGuild.js').InviteGuild; exports.InviteGuild = require('./structures/InviteGuild.js').InviteGuild;
exports.LabelComponent = require('./structures/LabelComponent.js').LabelComponent;
exports.MediaChannel = require('./structures/MediaChannel.js').MediaChannel; exports.MediaChannel = require('./structures/MediaChannel.js').MediaChannel;
exports.MediaGalleryComponent = require('./structures/MediaGalleryComponent.js').MediaGalleryComponent; exports.MediaGalleryComponent = require('./structures/MediaGalleryComponent.js').MediaGalleryComponent;
exports.MediaGalleryItem = require('./structures/MediaGalleryItem.js').MediaGalleryItem; exports.MediaGalleryItem = require('./structures/MediaGalleryItem.js').MediaGalleryItem;

View File

@@ -0,0 +1,54 @@
'use strict';
const { createComponent } = require('../util/Components.js');
const { Component } = require('./Component.js');
/**
* Represents a label component
*
* @extends {Component}
*/
class LabelComponent extends Component {
constructor({ component, ...data }) {
super(data);
/**
* The component in this label
*
* @type {Component}
* @readonly
*/
this.component = createComponent(component);
}
/**
* The label of the component
*
* @type {string}
* @readonly
*/
get label() {
return this.data.label;
}
/**
* The description of this component
*
* @type {?string}
* @readonly
*/
get description() {
return this.data.description ?? null;
}
/**
* Returns the API-compatible JSON for this component
*
* @returns {APILabelComponent}
*/
toJSON() {
return { ...this.data, component: this.component.toJSON() };
}
}
exports.LabelComponent = LabelComponent;

View File

@@ -12,7 +12,7 @@ class ModalSubmitFields {
/** /**
* The components within the modal * The components within the modal
* *
* @type {ActionRowModalData[]} * @type {Array<ActionRowModalData | LabelModalData>}
*/ */
this.components = components; this.components = components;
@@ -22,7 +22,16 @@ class ModalSubmitFields {
* @type {Collection<string, ModalData>} * @type {Collection<string, ModalData>}
*/ */
this.fields = components.reduce((accumulator, next) => { this.fields = components.reduce((accumulator, next) => {
for (const component of next.components) accumulator.set(component.customId, component); // NOTE: for legacy support of action rows in modals, which has `components`
if ('components' in next) {
for (const component of next.components) accumulator.set(component.customId, component);
}
// For label component
if ('component' in next) {
accumulator.set(next.component.customId, next.component);
}
return accumulator; return accumulator;
}, new Collection()); }, new Collection());
} }
@@ -54,6 +63,16 @@ class ModalSubmitFields {
getTextInputValue(customId) { getTextInputValue(customId) {
return this.getField(customId, ComponentType.TextInput).value; return this.getField(customId, ComponentType.TextInput).value;
} }
/**
* Gets the values of a string select component given a custom id
*
* @param {string} customId The custom id of the string select component
* @returns {string[]}
*/
getStringSelectValues(customId) {
return this.getField(customId, ComponentType.StringSelect).values;
}
} }
exports.ModalSubmitFields = ModalSubmitFields; exports.ModalSubmitFields = ModalSubmitFields;

View File

@@ -9,16 +9,38 @@ const { InteractionResponses } = require('./interfaces/InteractionResponses.js')
const getMessage = lazy(() => require('./Message.js').Message); const getMessage = lazy(() => require('./Message.js').Message);
/** /**
* @typedef {Object} ModalData * @typedef {Object} BaseModalData
* @property {string} value The value of the field
* @property {ComponentType} type The component type of the field * @property {ComponentType} type The component type of the field
* @property {string} customId The custom id of the field * @property {string} customId The custom id of the field
* @property {number} id The id of the field
*/
/**
* @typedef {BaseModalData} TextInputModalData
* @property {string} value The value of the field
*/
/**
* @typedef {BaseModalData} StringSelectModalData
* @property {string[]} values The values of the field
*/
/**
* @typedef {TextInputModalData | StringSelectModalData} ModalData
*/
/**
* @typedef {Object} LabelModalData
* @property {ModalData} component The component within the label
* @property {ComponentType} type The component type of the label
* @property {number} id The id of the label
*/ */
/** /**
* @typedef {Object} ActionRowModalData * @typedef {Object} ActionRowModalData
* @property {ModalData[]} components The components of this action row * @property {TextInputModalData[]} components The components of this action row
* @property {ComponentType} type The component type of the action row * @property {ComponentType} type The component type of the action row
* @property {number} id The id of the action row
*/ */
/** /**
@@ -51,7 +73,7 @@ class ModalSubmitInteraction extends BaseInteraction {
/** /**
* The components within the modal * The components within the modal
* *
* @type {ActionRowModalData[]} * @type {Array<ActionRowModalData | LabelModalData>}
*/ */
this.components = data.data.components?.map(component => ModalSubmitInteraction.transformComponent(component)); this.components = data.data.components?.map(component => ModalSubmitInteraction.transformComponent(component));
@@ -96,18 +118,35 @@ class ModalSubmitInteraction extends BaseInteraction {
* *
* @param {*} rawComponent The data to transform * @param {*} rawComponent The data to transform
* @returns {ModalData[]} * @returns {ModalData[]}
* @private
*/ */
static transformComponent(rawComponent) { static transformComponent(rawComponent) {
return rawComponent.components if ('components' in rawComponent) {
? { return {
type: rawComponent.type, type: rawComponent.type,
components: rawComponent.components.map(component => this.transformComponent(component)), id: rawComponent.id,
} components: rawComponent.components.map(component => this.transformComponent(component)),
: { };
value: rawComponent.value, }
type: rawComponent.type,
customId: rawComponent.custom_id, if ('component' in rawComponent) {
}; return {
type: rawComponent.type,
id: rawComponent.id,
component: this.transformComponent(rawComponent.component),
};
}
const data = {
type: rawComponent.type,
customId: rawComponent.custom_id,
id: rawComponent.id,
};
if (rawComponent.value) data.value = rawComponent.value;
if (rawComponent.values) data.values = rawComponent.values;
return data;
} }
/** /**

View File

@@ -9,13 +9,6 @@ const { InteractionCallbackResponse } = require('../InteractionCallbackResponse.
const { InteractionCollector } = require('../InteractionCollector.js'); const { InteractionCollector } = require('../InteractionCollector.js');
const { MessagePayload } = require('../MessagePayload.js'); const { MessagePayload } = require('../MessagePayload.js');
/**
* @typedef {Object} ModalComponentData
* @property {string} title The title of the modal
* @property {string} customId The custom id of the modal
* @property {ActionRow[]} components The components within this modal
*/
/** /**
* Interface for classes that support shared interaction response types. * Interface for classes that support shared interaction response types.
* *

View File

@@ -15,6 +15,24 @@ const { ComponentType } = require('discord-api-types/v10');
* @property {ComponentData[]} components The components in this action row * @property {ComponentData[]} components The components in this action row
*/ */
/**
* @typedef {Object} ModalComponentData
* @property {string} title The title of the modal
* @property {string} customId The custom id of the modal
* @property {LabelData[]} components The components within this modal
*/
/**
* @typedef {StringSelectMenuComponentData|TextInputComponentData} ComponentInLabelData
*/
/**
* @typedef {BaseComponentData} LabelData
* @property {string} label The label to use
* @property {string} [description] The optional description for the label
* @property {ComponentInLabelData} component The component within the label
*/
/** /**
* @typedef {BaseComponentData} ButtonComponentData * @typedef {BaseComponentData} ButtonComponentData
* @property {ButtonStyle} style The style of the button * @property {ButtonStyle} style The style of the button
@@ -25,6 +43,17 @@ const { ComponentType } = require('discord-api-types/v10');
* @property {string} [url] The URL of the button * @property {string} [url] The URL of the button
*/ */
/**
* @typedef {BaseComponentData} StringSelectMenuComponentData
* @property {string} customId The custom id of the select menu
* @property {boolean} [disabled] Whether the select menu is disabled or not
* @property {number} [maxValues] The maximum amount of options that can be selected
* @property {number} [minValues] The minimum amount of options that can be selected
* @property {SelectMenuComponentOptionData[]} [options] The options in this select menu
* @property {string} [placeholder] The placeholder of the select menu
* @property {boolean} [required] Whether this component is required in modals
*/
/** /**
* @typedef {Object} SelectMenuComponentOptionData * @typedef {Object} SelectMenuComponentOptionData
* @property {string} label The label of the option * @property {string} label The label of the option
@@ -52,7 +81,6 @@ const { ComponentType } = require('discord-api-types/v10');
* @typedef {BaseComponentData} TextInputComponentData * @typedef {BaseComponentData} TextInputComponentData
* @property {string} customId The custom id of the text input * @property {string} customId The custom id of the text input
* @property {TextInputStyle} style The style of the text input * @property {TextInputStyle} style The style of the text input
* @property {string} label The text that appears on top of the text input field
* @property {number} [minLength] The minimum number of characters that can be entered in the text input * @property {number} [minLength] The minimum number of characters that can be entered in the text input
* @property {number} [maxLength] The maximum number of characters that can be entered in the text input * @property {number} [maxLength] The maximum number of characters that can be entered in the text input
* @property {boolean} [required] Whether or not the text input is required or not * @property {boolean} [required] Whether or not the text input is required or not
@@ -186,6 +214,7 @@ const { ChannelSelectMenuComponent } = require('../structures/ChannelSelectMenuC
const { Component } = require('../structures/Component.js'); const { Component } = require('../structures/Component.js');
const { ContainerComponent } = require('../structures/ContainerComponent.js'); const { ContainerComponent } = require('../structures/ContainerComponent.js');
const { FileComponent } = require('../structures/FileComponent.js'); const { FileComponent } = require('../structures/FileComponent.js');
const { LabelComponent } = require('../structures/LabelComponent.js');
const { MediaGalleryComponent } = require('../structures/MediaGalleryComponent.js'); const { MediaGalleryComponent } = require('../structures/MediaGalleryComponent.js');
const { MentionableSelectMenuComponent } = require('../structures/MentionableSelectMenuComponent.js'); const { MentionableSelectMenuComponent } = require('../structures/MentionableSelectMenuComponent.js');
const { RoleSelectMenuComponent } = require('../structures/RoleSelectMenuComponent.js'); const { RoleSelectMenuComponent } = require('../structures/RoleSelectMenuComponent.js');
@@ -213,4 +242,5 @@ const ComponentTypeToClass = {
[ComponentType.Section]: SectionComponent, [ComponentType.Section]: SectionComponent,
[ComponentType.Separator]: SeparatorComponent, [ComponentType.Separator]: SeparatorComponent,
[ComponentType.Thumbnail]: ThumbnailComponent, [ComponentType.Thumbnail]: ThumbnailComponent,
[ComponentType.Label]: LabelComponent,
}; };

View File

@@ -50,6 +50,7 @@ import {
APIInteractionDataResolvedChannel, APIInteractionDataResolvedChannel,
APIInteractionDataResolvedGuildMember, APIInteractionDataResolvedGuildMember,
APIInteractionGuildMember, APIInteractionGuildMember,
APILabelComponent,
APIMediaGalleryComponent, APIMediaGalleryComponent,
APIMediaGalleryItem, APIMediaGalleryItem,
APIMentionableSelectComponent, APIMentionableSelectComponent,
@@ -65,6 +66,7 @@ import {
APIMessageTopLevelComponent, APIMessageTopLevelComponent,
APIMessageUserSelectInteractionData, APIMessageUserSelectInteractionData,
APIModalComponent, APIModalComponent,
APIModalInteractionResponseCallbackComponent,
APIModalInteractionResponseCallbackData, APIModalInteractionResponseCallbackData,
APIModalSubmitInteraction, APIModalSubmitInteraction,
APIOverwrite, APIOverwrite,
@@ -248,6 +250,7 @@ export class Activity {
export type ActivityFlagsString = keyof typeof ActivityFlags; export type ActivityFlagsString = keyof typeof ActivityFlags;
export interface BaseComponentData { export interface BaseComponentData {
id?: number;
type: ComponentType; type: ComponentType;
} }
@@ -260,17 +263,22 @@ export type MessageActionRowComponentData =
| StringSelectMenuComponentData | StringSelectMenuComponentData
| UserSelectMenuComponentData; | UserSelectMenuComponentData;
export type ModalActionRowComponentData = JSONEncodable<APIComponentInModalActionRow> | TextInputComponentData; export type ActionRowComponentData = MessageActionRowComponentData;
export type ActionRowComponentData = MessageActionRowComponentData | ModalActionRowComponentData; export type ActionRowComponent = MessageActionRowComponent;
export type ActionRowComponent = MessageActionRowComponent | ModalActionRowComponent;
export interface ActionRowData<ComponentType extends ActionRowComponentData | JSONEncodable<APIComponentInActionRow>> export interface ActionRowData<ComponentType extends ActionRowComponentData | JSONEncodable<APIComponentInActionRow>>
extends BaseComponentData { extends BaseComponentData {
components: readonly ComponentType[]; components: readonly ComponentType[];
} }
export type ComponentInLabelData = StringSelectMenuComponentData | TextInputComponentData;
export interface LabelData extends BaseComponentData {
component: ComponentInLabelData;
description?: string;
label: string;
}
export type MessageActionRowComponent = export type MessageActionRowComponent =
| ButtonComponent | ButtonComponent
| ChannelSelectMenuComponent | ChannelSelectMenuComponent
@@ -278,12 +286,11 @@ export type MessageActionRowComponent =
| RoleSelectMenuComponent | RoleSelectMenuComponent
| StringSelectMenuComponent | StringSelectMenuComponent
| UserSelectMenuComponent; | UserSelectMenuComponent;
export type ModalActionRowComponent = TextInputComponent;
export class ActionRow<ComponentType extends MessageActionRowComponent | ModalActionRowComponent> extends Component< export class ActionRow<ComponentType extends MessageActionRowComponent> extends Component<
APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow> APIActionRowComponent<APIComponentInMessageActionRow>
> { > {
private constructor(data: APIActionRowComponent<APIComponentInMessageActionRow | APIComponentInModalActionRow>); private constructor(data: APIActionRowComponent<APIComponentInMessageActionRow>);
public readonly components: ComponentType[]; public readonly components: ComponentType[];
public toJSON(): APIActionRowComponent<ReturnType<ComponentType['toJSON']>>; public toJSON(): APIActionRowComponent<ReturnType<ComponentType['toJSON']>>;
} }
@@ -740,6 +747,12 @@ export class TextInputComponent extends Component<APITextInputComponent> {
public get value(): string; public get value(): string;
} }
export class LabelComponent extends Component<APILabelComponent> {
public component: StringSelectMenuComponent | TextInputComponent;
public get label(): string;
public get description(): string | null;
}
export class BaseSelectMenuComponent<Data extends APISelectMenuComponent> extends Component<Data> { export class BaseSelectMenuComponent<Data extends APISelectMenuComponent> extends Component<Data> {
protected constructor(data: Data); protected constructor(data: Data);
public get placeholder(): string | null; public get placeholder(): string | null;
@@ -2527,36 +2540,48 @@ export interface MessageReactionEventDetails {
} }
export interface ModalComponentData { export interface ModalComponentData {
components: readonly ( components: readonly LabelData[];
| ActionRowData<ModalActionRowComponentData>
| JSONEncodable<APIActionRowComponent<APIComponentInModalActionRow>>
)[];
customId: string; customId: string;
title: string; title: string;
} }
export interface BaseModalData { export interface BaseModalData<Type extends ComponentType> {
customId: string; customId: string;
type: ComponentType; id: number;
type: Type;
} }
export interface TextInputModalData extends BaseModalData { export interface TextInputModalData extends BaseModalData<ComponentType.TextInput> {
type: ComponentType.TextInput;
value: string; value: string;
} }
export interface StringSelectModalData extends BaseModalData<ComponentType.StringSelect> {
values: readonly string[];
}
export type ModalData = StringSelectModalData | TextInputModalData;
export interface LabelModalData {
component: readonly ModalData[];
id: number;
type: ComponentType.Label;
}
export interface ActionRowModalData { export interface ActionRowModalData {
components: readonly TextInputModalData[]; components: readonly TextInputModalData[];
type: ComponentType.ActionRow; type: ComponentType.ActionRow;
} }
export class ModalSubmitFields { export class ModalSubmitFields {
private constructor(components: readonly (readonly ModalActionRowComponent[])[]); private constructor(components: readonly (ActionRowModalData | LabelModalData)[]);
public components: ActionRowModalData[]; public components: (ActionRowModalData | LabelModalData)[];
public fields: Collection<string, TextInputModalData>; public fields: Collection<string, StringSelectModalData | TextInputModalData>;
public getField<Type extends ComponentType>(customId: string, type: Type): TextInputModalData & { type: Type }; public getField<Type extends ComponentType>(
public getField(customId: string, type?: ComponentType): TextInputModalData; customId: string,
type: Type,
): { type: Type } & (StringSelectModalData | TextInputModalData);
public getField(customId: string, type?: ComponentType): StringSelectModalData | TextInputModalData;
public getTextInputValue(customId: string): string; public getTextInputValue(customId: string): string;
public getStringSelectValues(customId: string): readonly string[];
} }
export interface ModalMessageModalSubmitInteraction<Cached extends CacheType = CacheType> export interface ModalMessageModalSubmitInteraction<Cached extends CacheType = CacheType>
@@ -2579,7 +2604,7 @@ export class ModalSubmitInteraction<Cached extends CacheType = CacheType> extend
private constructor(client: Client<true>, data: APIModalSubmitInteraction); private constructor(client: Client<true>, data: APIModalSubmitInteraction);
public type: InteractionType.ModalSubmit; public type: InteractionType.ModalSubmit;
public readonly customId: string; public readonly customId: string;
public readonly components: ActionRowModalData[]; public readonly components: (ActionRowModalData | LabelModalData)[];
public readonly fields: ModalSubmitFields; public readonly fields: ModalSubmitFields;
public deferred: boolean; public deferred: boolean;
public ephemeral: boolean | null; public ephemeral: boolean | null;
@@ -3645,9 +3670,10 @@ export function verifyString(data: string, error?: typeof Error, errorMessage?:
export type ComponentData = export type ComponentData =
| ComponentInContainerData | ComponentInContainerData
| ComponentInLabelData
| ContainerComponentData | ContainerComponentData
| LabelData
| MessageActionRowComponentData | MessageActionRowComponentData
| ModalActionRowComponentData
| ThumbnailComponentData; | ThumbnailComponentData;
export interface SendSoundboardSoundOptions { export interface SendSoundboardSoundOptions {
@@ -6669,6 +6695,7 @@ export interface BaseSelectMenuComponentData extends BaseComponentData {
export interface StringSelectMenuComponentData extends BaseSelectMenuComponentData { export interface StringSelectMenuComponentData extends BaseSelectMenuComponentData {
options: readonly SelectMenuComponentOptionData[]; options: readonly SelectMenuComponentOptionData[];
required?: boolean;
type: ComponentType.StringSelect; type: ComponentType.StringSelect;
} }
@@ -6718,7 +6745,6 @@ export interface SelectMenuComponentOptionData {
export interface TextInputComponentData extends BaseComponentData { export interface TextInputComponentData extends BaseComponentData {
customId: string; customId: string;
label: string;
maxLength?: number; maxLength?: number;
minLength?: number; minLength?: number;
placeholder?: string; placeholder?: string;

View File

@@ -2584,15 +2584,30 @@ await chatInputInteraction.showModal({
custom_id: 'abc', custom_id: 'abc',
components: [ components: [
{ {
components: [ component: {
{ type: ComponentType.StringSelect,
custom_id: 'aa', id: 2,
label: 'label', custom_id: 'aa',
style: TextInputStyle.Short, options: [{ label: 'a', value: 'b' }],
type: ComponentType.TextInput, },
}, type: ComponentType.Label,
], label: 'yo',
type: ComponentType.ActionRow, },
],
});
await chatInputInteraction.showModal({
title: 'abc',
customId: 'abc',
components: [
{
type: ComponentType.Label,
component: {
type: ComponentType.TextInput,
style: TextInputStyle.Short,
customId: 'aa',
},
label: 'yo',
}, },
], ],
}); });