Skip to content

Commit ff628ee

Browse files
NeloBlivionpre-commit-ci[bot]LulalabyDA-344Dorukyum
authored
feat: Modal Selects and TextDisplays (#2858)
Signed-off-by: Lala Sabathil <[email protected]> Signed-off-by: UK <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lala Sabathil <[email protected]> Co-authored-by: DA344 <[email protected]> Co-authored-by: Dorukyum <[email protected]> Co-authored-by: Paillat <[email protected]> Co-authored-by: Ice Wolfy <[email protected]>
1 parent 233ca64 commit ff628ee

File tree

10 files changed

+280
-42
lines changed

10 files changed

+280
-42
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ These changes are available on the `master` branch, but have not yet been releas
6969
([#2818](https://github.com/Pycord-Development/pycord/pull/2818))
7070
- Added `Interaction.attachment_size_limit`.
7171
([#2854](https://github.com/Pycord-Development/pycord/pull/2854))
72+
- Added support for selects and text displays in modals.
73+
([#2858](https://github.com/Pycord-Development/pycord/pull/2858))
7274
- Added `AuditLogDiff.communication_disabled_until`.
7375
([#2883](https://github.com/Pycord-Development/pycord/pull/2883))
7476
- Added `discord.User.primary_guild` and the `PrimaryGuild` class.

discord/components.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from .types.components import ContainerComponent as ContainerComponentPayload
5151
from .types.components import FileComponent as FileComponentPayload
5252
from .types.components import InputText as InputTextComponentPayload
53+
from .types.components import LabelComponent as LabelComponentPayload
5354
from .types.components import MediaGalleryComponent as MediaGalleryComponentPayload
5455
from .types.components import MediaGalleryItem as MediaGalleryItemPayload
5556
from .types.components import SectionComponent as SectionComponentPayload
@@ -76,6 +77,7 @@
7677
"FileComponent",
7778
"Separator",
7879
"Container",
80+
"Label",
7981
)
8082

8183
C = TypeVar("C", bound="Component")
@@ -375,6 +377,10 @@ class SelectMenu(Component):
375377
Added support for :attr:`ComponentType.user_select`, :attr:`ComponentType.role_select`,
376378
:attr:`ComponentType.mentionable_select`, and :attr:`ComponentType.channel_select`.
377379
380+
.. versionchanged:: 2.7
381+
382+
Added the :attr:`required` attribute for use in modals.
383+
378384
Attributes
379385
----------
380386
type: :class:`ComponentType`
@@ -398,7 +404,9 @@ class SelectMenu(Component):
398404
Will be an empty list for all component types
399405
except for :attr:`ComponentType.channel_select`.
400406
disabled: :class:`bool`
401-
Whether the select is disabled or not.
407+
Whether the select is disabled or not. Not usable in modals. Defaults to ``False``.
408+
required: Optional[:class:`bool`]
409+
Whether the select is required or not. Only useable in modals. Defaults to ``True``.
402410
"""
403411

404412
__slots__: tuple[str, ...] = (
@@ -409,6 +417,7 @@ class SelectMenu(Component):
409417
"options",
410418
"channel_types",
411419
"disabled",
420+
"required",
412421
)
413422

414423
__repr_info__: ClassVar[tuple[str, ...]] = __slots__
@@ -428,6 +437,7 @@ def __init__(self, data: SelectMenuPayload):
428437
self.channel_types: list[ChannelType] = [
429438
try_enum(ChannelType, ct) for ct in data.get("channel_types", [])
430439
]
440+
self.required: bool | None = data.get("required")
431441

432442
def to_dict(self) -> SelectMenuPayload:
433443
payload: SelectMenuPayload = {
@@ -445,6 +455,8 @@ def to_dict(self) -> SelectMenuPayload:
445455
payload["channel_types"] = [ct.value for ct in self.channel_types]
446456
if self.placeholder:
447457
payload["placeholder"] = self.placeholder
458+
if self.required is not None:
459+
payload["required"] = self.required
448460

449461
return payload
450462

@@ -1037,6 +1049,55 @@ def walk_components(self) -> Iterator[Component]:
10371049
yield c
10381050

10391051

1052+
class Label(Component):
1053+
"""Represents a Label used in modals as the top-level component.
1054+
1055+
This is a component that allows you to add additional text to another component.
1056+
``component`` may only be:
1057+
1058+
- :class:`InputText`
1059+
- :class:`SelectMenu` (string)
1060+
1061+
This inherits from :class:`Component`.
1062+
1063+
.. versionadded:: 2.7
1064+
1065+
Attributes
1066+
----------
1067+
component: :class:`Component`
1068+
The component contained in this label. Currently supports :class:`InputText` and :class:`SelectMenu`.
1069+
label: :class:`str`
1070+
The main text associated with this label's ``component``, up to 45 characters.
1071+
description: Optional[:class:`str`]
1072+
The description associated with this label's ``component``, up to 100 characters.
1073+
"""
1074+
1075+
__slots__: tuple[str, ...] = ("component", "label", "description")
1076+
1077+
__repr_info__: ClassVar[tuple[str, ...]] = __slots__
1078+
versions: tuple[int, ...] = ()
1079+
1080+
def __init__(self, data: LabelComponentPayload):
1081+
self.type: ComponentType = try_enum(ComponentType, data["type"])
1082+
self.id: int = data["id"]
1083+
self.component: Component = _component_factory(data["component"])
1084+
self.label: str = data["label"]
1085+
self.description: str | None = data.get("description")
1086+
1087+
def to_dict(self) -> LabelComponentPayload:
1088+
payload = {
1089+
"type": int(self.type),
1090+
"id": self.id,
1091+
"component": self.component.to_dict(),
1092+
"label": self.label,
1093+
"description": self.description,
1094+
}
1095+
return payload
1096+
1097+
def walk_components(self) -> Iterator[Component]:
1098+
yield from [self.component]
1099+
1100+
10401101
COMPONENT_MAPPINGS = {
10411102
1: ActionRow,
10421103
2: Button,
@@ -1053,6 +1114,7 @@ def walk_components(self) -> Iterator[Component]:
10531114
13: FileComponent,
10541115
14: Separator,
10551116
17: Container,
1117+
18: Label,
10561118
}
10571119

10581120
STATE_COMPONENTS = (Section, Container, Thumbnail, MediaGallery, FileComponent)

discord/interactions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ class Interaction:
193193
"view",
194194
"modal",
195195
"attachment_size_limit",
196+
"_raw_data",
196197
"_channel_data",
197198
"_message_data",
198199
"_guild_data",
@@ -215,6 +216,7 @@ def __init__(self, *, data: InteractionPayload, state: ConnectionState):
215216
self._from_data(data)
216217

217218
def _from_data(self, data: InteractionPayload):
219+
self._raw_data: InteractionPayload = data
218220
self.id: int = int(data["id"])
219221
self.type: InteractionType = try_enum(InteractionType, data["type"])
220222
self.data: InteractionData | None = data.get("data")

discord/types/components.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from .emoji import PartialEmoji
3434
from .snowflake import Snowflake
3535

36-
ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17]
36+
ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18]
3737
ButtonStyle = Literal[1, 2, 3, 4, 5, 6]
3838
InputTextStyle = Literal[1, 2]
3939
SeparatorSpacingSize = Literal[1, 2]
@@ -69,7 +69,7 @@ class InputText(BaseComponent):
6969
type: Literal[4]
7070
style: InputTextStyle
7171
custom_id: str
72-
label: str
72+
label: NotRequired[str]
7373

7474

7575
class SelectOption(TypedDict):
@@ -89,6 +89,7 @@ class SelectMenu(BaseComponent):
8989
options: NotRequired[list[SelectOption]]
9090
type: Literal[3, 5, 6, 7, 8]
9191
custom_id: str
92+
required: NotRequired[bool]
9293

9394

9495
class TextDisplayComponent(BaseComponent):
@@ -151,6 +152,13 @@ class ContainerComponent(BaseComponent):
151152
components: list[AllowedContainerComponents]
152153

153154

155+
class LabelComponent(BaseComponent):
156+
type: Literal[18]
157+
label: str
158+
description: NotRequired[str]
159+
component: SelectMenu | InputText
160+
161+
154162
Component = Union[ActionRow, ButtonComponent, SelectMenu, InputText]
155163

156164

discord/ui/input_text.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
from ..components import InputText as InputTextComponent
77
from ..enums import ComponentType, InputTextStyle
88

9-
__all__ = ("InputText",)
9+
__all__ = ("InputText", "TextInput")
1010

1111
if TYPE_CHECKING:
12+
from ..interactions import Interaction
1213
from ..types.components import InputText as InputTextComponentPayload
1314

1415

@@ -26,6 +27,11 @@ class InputText:
2627
label: :class:`str`
2728
The label for the input text field.
2829
Must be 45 characters or fewer.
30+
description: Optional[:class:`str`]
31+
The description for the input text field.
32+
Must be 100 characters or fewer.
33+
34+
.. versionadded:: 2.7
2935
placeholder: Optional[:class:`str`]
3036
The placeholder text that is shown if nothing is selected, if any.
3137
Must be 100 characters or fewer.
@@ -58,6 +64,7 @@ class InputText:
5864
"max_length",
5965
"custom_id",
6066
"id",
67+
"description",
6168
)
6269

6370
def __init__(
@@ -73,10 +80,13 @@ def __init__(
7380
value: str | None = None,
7481
row: int | None = None,
7582
id: int | None = None,
83+
description: str | None = None,
7684
):
7785
super().__init__()
7886
if len(str(label)) > 45:
7987
raise ValueError("label must be 45 characters or fewer")
88+
if description and len(description) > 100:
89+
raise ValueError("description must be 100 characters or fewer")
8090
if min_length and (min_length < 0 or min_length > 4000):
8191
raise ValueError("min_length must be between 0 and 4000")
8292
if max_length and (max_length < 0 or max_length > 4000):
@@ -90,6 +100,7 @@ def __init__(
90100
f"expected custom_id to be str, not {custom_id.__class__.__name__}"
91101
)
92102
custom_id = os.urandom(16).hex() if custom_id is None else custom_id
103+
self.description: str | None = description
93104

94105
self._underlying = InputTextComponent._raw_construct(
95106
type=ComponentType.input_text,
@@ -236,3 +247,12 @@ def to_component_dict(self) -> InputTextComponentPayload:
236247

237248
def refresh_state(self, data) -> None:
238249
self._input_value = data["value"]
250+
251+
def refresh_from_modal(self, interaction: Interaction, data: dict) -> None:
252+
return self.refresh_state(data)
253+
254+
def uses_label(self) -> bool:
255+
return self.description is not None
256+
257+
258+
TextInput = InputText

discord/ui/item.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ def refresh_component(self, component: Component) -> None:
9090
def refresh_state(self, interaction: Interaction) -> None:
9191
return None
9292

93+
def refresh_from_modal(self, interaction: Interaction, data: dict) -> None:
94+
return None
95+
9396
@classmethod
9497
def from_component(cls: type[I], component: Component) -> I:
9598
return cls()
@@ -107,6 +110,9 @@ def is_storable(self) -> bool:
107110
def is_persistent(self) -> bool:
108111
return not self.is_dispatchable() or self._provided_custom_id
109112

113+
def uses_label(self) -> bool:
114+
return False
115+
110116
def copy_text(self) -> str:
111117
return ""
112118

0 commit comments

Comments
 (0)