Skip to content

Commit d7050a7

Browse files
shiftinvSnipy7374onerandomusername
authored
feat: new modal components (#1321)
Signed-off-by: vi <[email protected]> Co-authored-by: Snipy7374 <[email protected]> Co-authored-by: arielle <[email protected]>
1 parent bb27273 commit d7050a7

28 files changed

+708
-157
lines changed

changelog/1321.deprecate.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Using :class:`~ui.ActionRow`\s (or plain :class:`~ui.TextInput`\s, which implicitly get wrapped in action rows) in modals is now deprecated in favor of :class:`ui.Label`.
2+
- This deprecates :meth:`ui.ActionRow.add_text_input`, :meth:`ui.ActionRow.with_modal_components`.
3+
- Using :class:`TextInput.label` is also deprecated in favor of ``Label("<text>", TextInput(...))``.

changelog/1321.feature.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Add support for new components in :class:`~ui.Modal`\s.
2+
- Add new top-level :class:`ui.Label` component, which wraps other components (currently :class:`~ui.TextInput` and :class:`~ui.StringSelect`) in a modal with a label and description.
3+
- :class:`ui.StringSelect` can now be used in modals when placed inside a :class:`ui.Label`.
4+
- The new modal-specific :attr:`ui.StringSelect.required` field can be used to make a select menu optional.
5+
- The values provided by users on modal submit are available in :attr:`ModalInteraction.values`.

disnake/components.py

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
ComponentType as ComponentTypeLiteral,
5050
ContainerComponent as ContainerComponentPayload,
5151
FileComponent as FileComponentPayload,
52+
LabelComponent as LabelComponentPayload,
5253
MediaGalleryComponent as MediaGalleryComponentPayload,
5354
MediaGalleryItem as MediaGalleryItemPayload,
5455
MentionableSelectMenu as MentionableSelectMenuPayload,
@@ -89,6 +90,7 @@
8990
"FileComponent",
9091
"Separator",
9192
"Container",
93+
"Label",
9294
)
9395

9496
# miscellaneous components-related type aliases
@@ -135,6 +137,12 @@
135137
"Separator",
136138
]
137139

140+
# valid `Label.component` types
141+
LabelChildComponent = Union[
142+
"TextInput",
143+
"StringSelectMenu",
144+
]
145+
138146
# valid `Message.components` item types (v1/v2)
139147
MessageTopLevelComponentV1: TypeAlias = "ActionRow[ActionRowMessageComponent]"
140148
MessageTopLevelComponentV2 = Union[
@@ -164,6 +172,7 @@ class Component:
164172
- :class:`FileComponent`
165173
- :class:`Separator`
166174
- :class:`Container`
175+
- :class:`Label`
167176
168177
This class is abstract and cannot be instantiated.
169178
@@ -386,6 +395,11 @@ class BaseSelectMenu(Component):
386395
Only available for auto-populated select menus.
387396
388397
.. versionadded:: 2.10
398+
required: :class:`bool`
399+
Whether the select menu is required. Only applies to components in modals.
400+
Defaults to ``True``.
401+
402+
.. versionadded:: 2.11
389403
id: :class:`int`
390404
The numeric identifier for the component.
391405
This is always present in components received from the API,
@@ -401,6 +415,7 @@ class BaseSelectMenu(Component):
401415
"max_values",
402416
"disabled",
403417
"default_values",
418+
"required",
404419
)
405420

406421
# FIXME: this isn't pretty; we should decouple __repr__ from slots
@@ -424,6 +439,7 @@ def __init__(self, data: AnySelectMenuPayload) -> None:
424439
self.default_values: List[SelectDefaultValue] = [
425440
SelectDefaultValue._from_dict(d) for d in (data.get("default_values") or [])
426441
]
442+
self.required: bool = data.get("required", True)
427443

428444
def to_dict(self) -> BaseSelectMenuPayload:
429445
payload: BaseSelectMenuPayload = {
@@ -433,6 +449,7 @@ def to_dict(self) -> BaseSelectMenuPayload:
433449
"min_values": self.min_values,
434450
"max_values": self.max_values,
435451
"disabled": self.disabled,
452+
"required": self.required,
436453
}
437454

438455
if self.placeholder:
@@ -472,6 +489,11 @@ class StringSelectMenu(BaseSelectMenu):
472489
Whether the select menu is disabled or not.
473490
options: List[:class:`SelectOption`]
474491
A list of options that can be selected in this select menu.
492+
required: :class:`bool`
493+
Whether the select menu is required. Only applies to components in modals.
494+
Defaults to ``True``.
495+
496+
.. versionadded:: 2.11
475497
id: :class:`int`
476498
The numeric identifier for the component.
477499
This is always present in components received from the API,
@@ -531,6 +553,11 @@ class UserSelectMenu(BaseSelectMenu):
531553
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
532554
533555
.. versionadded:: 2.10
556+
required: :class:`bool`
557+
Whether the select menu is required. Only applies to components in modals.
558+
Defaults to ``True``.
559+
560+
.. versionadded:: 2.11
534561
id: :class:`int`
535562
The numeric identifier for the component.
536563
This is always present in components received from the API,
@@ -577,6 +604,11 @@ class RoleSelectMenu(BaseSelectMenu):
577604
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
578605
579606
.. versionadded:: 2.10
607+
required: :class:`bool`
608+
Whether the select menu is required. Only applies to components in modals.
609+
Defaults to ``True``.
610+
611+
.. versionadded:: 2.11
580612
id: :class:`int`
581613
The numeric identifier for the component.
582614
This is always present in components received from the API,
@@ -623,6 +655,11 @@ class MentionableSelectMenu(BaseSelectMenu):
623655
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
624656
625657
.. versionadded:: 2.10
658+
required: :class:`bool`
659+
Whether the select menu is required. Only applies to components in modals.
660+
Defaults to ``True``.
661+
662+
.. versionadded:: 2.11
626663
id: :class:`int`
627664
The numeric identifier for the component.
628665
This is always present in components received from the API,
@@ -672,6 +709,11 @@ class ChannelSelectMenu(BaseSelectMenu):
672709
If set, the number of items must be within the bounds set by ``min_values`` and ``max_values``.
673710
674711
.. versionadded:: 2.10
712+
required: :class:`bool`
713+
Whether the select menu is required. Only applies to components in modals.
714+
Defaults to ``True``.
715+
716+
.. versionadded:: 2.11
675717
id: :class:`int`
676718
The numeric identifier for the component.
677719
This is always present in components received from the API,
@@ -861,6 +903,10 @@ class TextInput(Component):
861903
The style of the text input.
862904
label: Optional[:class:`str`]
863905
The label of the text input.
906+
907+
.. deprecated:: 2.11
908+
Deprecated in favor of :class:`Label`.
909+
864910
custom_id: :class:`str`
865911
The ID of the text input that gets received during an interaction.
866912
placeholder: Optional[:class:`str`]
@@ -902,7 +948,7 @@ def __init__(self, data: TextInputPayload) -> None:
902948
self.style: TextInputStyle = try_enum(
903949
TextInputStyle, data.get("style", TextInputStyle.short.value)
904950
)
905-
self.label: Optional[str] = data.get("label")
951+
self.label: Optional[str] = data.get("label") # deprecated
906952
self.placeholder: Optional[str] = data.get("placeholder")
907953
self.value: Optional[str] = data.get("value")
908954
self.required: bool = data.get("required", True)
@@ -914,7 +960,7 @@ def to_dict(self) -> TextInputPayload:
914960
"type": self.type.value,
915961
"id": self.id,
916962
"style": self.style.value,
917-
"label": cast("str", self.label),
963+
"label": self.label,
918964
"custom_id": self.custom_id,
919965
"required": self.required,
920966
}
@@ -1420,6 +1466,65 @@ def accent_color(self) -> Optional[Colour]:
14201466
return self.accent_colour
14211467

14221468

1469+
class Label(Component):
1470+
"""Represents a label from the Discord Bot UI Kit.
1471+
1472+
This wraps other components with a label and an optional description,
1473+
and can only be used in modals.
1474+
1475+
.. versionadded:: 2.11
1476+
1477+
.. note::
1478+
1479+
The user constructible and usable type to create a label is
1480+
:class:`disnake.ui.Label`, not this one.
1481+
1482+
Attributes
1483+
----------
1484+
text: :class:`str`
1485+
The label text.
1486+
description: Optional[:class:`str`]
1487+
The description text for the label.
1488+
component: Union[:class:`TextInput`, :class:`StringSelectMenu`]
1489+
The component within the label.
1490+
id: :class:`int`
1491+
The numeric identifier for the component.
1492+
This is always present in components received from the API,
1493+
and unique within a message.
1494+
"""
1495+
1496+
__slots__: Tuple[str, ...] = (
1497+
"text",
1498+
"description",
1499+
"component",
1500+
)
1501+
1502+
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
1503+
1504+
def __init__(self, data: LabelComponentPayload) -> None:
1505+
self.type: Literal[ComponentType.label] = ComponentType.label
1506+
self.id = data.get("id", 0)
1507+
1508+
self.text: str = data["label"]
1509+
self.description: Optional[str] = data.get("description")
1510+
1511+
component = _component_factory(data["component"])
1512+
self.component: LabelChildComponent = component # type: ignore
1513+
1514+
def to_dict(self) -> LabelComponentPayload:
1515+
payload: LabelComponentPayload = {
1516+
"type": self.type.value,
1517+
"id": self.id,
1518+
"label": self.text,
1519+
"component": self.component.to_dict(),
1520+
}
1521+
1522+
if self.description is not None:
1523+
payload["description"] = self.description
1524+
1525+
return payload
1526+
1527+
14231528
# types of components that are allowed in a message's action rows;
14241529
# see also `ActionRowMessageComponent` type alias
14251530
VALID_ACTION_ROW_MESSAGE_COMPONENT_TYPES: Final = (
@@ -1467,6 +1572,7 @@ def handle_media_item_input(value: MediaItemInput) -> UnfurledMediaItem:
14671572
ComponentType.file.value: FileComponent,
14681573
ComponentType.separator.value: Separator,
14691574
ComponentType.container.value: Container,
1575+
ComponentType.label.value: Label,
14701576
}
14711577

14721578

disnake/enums.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,11 @@ class ComponentType(Enum):
12651265
container = 17
12661266
"""Represents a Components V2 container component.
12671267
1268+
.. versionadded:: 2.11
1269+
"""
1270+
label = 18
1271+
"""Represents a label component.
1272+
12681273
.. versionadded:: 2.11
12691274
"""
12701275

disnake/interactions/base.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
List,
1313
Mapping,
1414
Optional,
15+
Sequence,
1516
Tuple,
1617
TypeVar,
1718
Union,
@@ -75,15 +76,18 @@
7576
from ..mentions import AllowedMentions
7677
from ..poll import Poll
7778
from ..state import ConnectionState
78-
from ..types.components import Modal as ModalPayload
79+
from ..types.components import (
80+
Modal as ModalPayload,
81+
ModalTopLevelComponent as ModalTopLevelComponentPayload,
82+
)
7983
from ..types.interactions import (
8084
ApplicationCommandOptionChoice as ApplicationCommandOptionChoicePayload,
8185
Interaction as InteractionPayload,
8286
InteractionDataResolved as InteractionDataResolvedPayload,
8387
)
8488
from ..types.snowflake import Snowflake
8589
from ..types.user import User as UserPayload
86-
from ..ui._types import MessageComponents, ModalComponents
90+
from ..ui._types import MessageComponents, ModalComponents, ModalTopLevelComponent
8791
from ..ui.modal import Modal
8892
from ..ui.view import View
8993
from .message import MessageInteraction
@@ -1499,11 +1503,18 @@ async def send_modal(
14991503
custom_id: :class:`str`
15001504
The ID of the modal that gets received during an interaction.
15011505
This cannot be mixed with the ``modal`` parameter.
1502-
components: |components_type|
1506+
components: |modal_components_type|
15031507
The components to display in the modal. A maximum of 5.
1504-
Currently only supports :class:`ui.TextInput` (optionally inside :class:`ui.ActionRow`).
1508+
Currently only supports :class:`.ui.TextInput` and
1509+
:class:`.ui.StringSelect`, wrapped in :class:`.ui.Label`\\s.
1510+
15051511
This cannot be mixed with the ``modal`` parameter.
15061512
1513+
.. versionchanged:: 2.11
1514+
Using action rows in modals or passing :class:`.ui.TextInput` directly
1515+
(which implicitly wraps it in an action row) is deprecated.
1516+
Use :class:`.ui.TextInput` inside a :class:`.ui.Label` instead.
1517+
15071518
Raises
15081519
------
15091520
TypeError
@@ -1533,14 +1544,17 @@ async def send_modal(
15331544
if modal is not None:
15341545
modal_data = modal.to_components()
15351546
elif title and components and custom_id:
1536-
rows = normalize_components(components)
1537-
if len(rows) > 5:
1547+
items: Sequence[ModalTopLevelComponent] = normalize_components(components, modal=True)
1548+
if len(items) > 5:
15381549
raise ValueError("Maximum number of components exceeded.")
15391550

15401551
modal_data = {
15411552
"title": title,
15421553
"custom_id": custom_id,
1543-
"components": [component.to_component_dict() for component in rows],
1554+
"components": cast(
1555+
"List[ModalTopLevelComponentPayload]",
1556+
[component.to_component_dict() for component in items],
1557+
),
15441558
}
15451559
else:
15461560
raise TypeError("Either modal or title, custom_id, components must be provided")

0 commit comments

Comments
 (0)