diff --git a/discord/components.py b/discord/components.py index 0a09a2d56..7e89f8ffc 100644 --- a/discord/components.py +++ b/discord/components.py @@ -33,6 +33,8 @@ if TYPE_CHECKING: from .types.components import ( Component as ComponentPayload, ButtonComponent as ButtonComponentPayload, + SelectMenu as SelectMenuPayload, + SelectOption as SelectOptionPayload, ActionRow as ActionRowPayload, ) @@ -41,6 +43,8 @@ __all__ = ( 'Component', 'ActionRow', 'Button', + 'SelectMenu', + 'SelectOption', ) C = TypeVar('C', bound='Component') @@ -53,6 +57,7 @@ class Component: - :class:`ActionRow` - :class:`Button` + - :class:`SelectMenu` This class is abstract and cannot be instantiated. @@ -71,7 +76,7 @@ class Component: def __repr__(self) -> str: attrs = ' '.join(f'{key}={getattr(self, key)!r}' for key in self.__repr_info__) - return f'<{self.__class__.__name__} type={self.type!r} {attrs}>' + return f'<{self.__class__.__name__} {attrs}>' @classmethod def _raw_construct(cls: Type[C], **kwargs) -> C: @@ -94,6 +99,8 @@ class ActionRow(Component): This is a component that holds up to 5 children components in a row. + This inherits from :class:`Component`. + .. versionadded:: 2.0 Attributes @@ -186,12 +193,155 @@ class Button(Component): return payload # type: ignore +class SelectMenu(Component): + """Represents a select menu from the Discord Bot UI Kit. + + A select menu is functionally the same as a dropdown, however + on mobile it renders a bit differently. + + .. versionadded:: 2.0 + + Attributes + ------------ + custom_id: Optional[:class:`str`] + The ID of the select menu that gets received during an interaction. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + options: List[:class:`SelectOption`] + A list of options that can be selected in this menu. + """ + + __slots__: Tuple[str, ...] = ( + 'custom_id', + 'placeholder', + 'min_values', + 'max_values', + 'options', + ) + + __repr_info__: ClassVar[Tuple[str, ...]] = __slots__ + + def __init__(self, data: SelectMenuPayload): + self.type = ComponentType.select + self.custom_id: str = data['custom_id'] + self.placeholder: Optional[str] = data.get('placeholder') + self.min_values: int = data.get('min_values', 1) + self.max_values: int = data.get('max_values', 1) + self.options: List[SelectOption] = [SelectOption.from_dict(option) for option in data.get('options', [])] + + def to_dict(self) -> SelectMenuPayload: + payload: SelectMenuPayload = { + 'type': self.type.value, + 'custom_id': self.custom_id, + 'min_values': self.min_values, + 'max_values': self.max_values, + 'options': [op.to_dict() for op in self.options], + } + + if self.placeholder: + payload['placeholder'] = self.placeholder + + return payload + + +class SelectOption: + """Represents a select menu's option. + + These can be created by users. + + .. versionadded:: 2.0 + + Attributes + ----------- + label: :class:`str` + The label of the option. This is displayed to users. + Can only be up to 25 characters. + value: :class:`str` + The value of the option. This is not displayed to users. + Can only be up to 100 characters. + description: Optional[:class:`str`] + An additional description of the option, if any. + Can only be up to 50 characters. + emoji: Optional[:class:`PartialEmoji`] + The emoji of the option, if available. + default: :class:`bool` + Whether this option is selected by default. + """ + + __slots__: Tuple[str, ...] = ( + 'label', + 'value', + 'description', + 'emoji', + 'default', + ) + + def __init__( + self, + *, + label: str, + value: str, + description: Optional[str] = None, + emoji: Optional[PartialEmoji] = None, + default: bool = False, + ) -> None: + self.label = label + self.value = value + self.description = description + self.emoji = emoji + self.default = default + + def __repr__(self) -> str: + return ( + f'' + ) + + @classmethod + def from_dict(cls, data: SelectOptionPayload) -> SelectOption: + try: + emoji = PartialEmoji.from_dict(data['emoji']) + except KeyError: + emoji = None + + return cls( + label=data['label'], + value=data['value'], + description=data.get('description'), + emoji=emoji, + default=data.get('default', False), + ) + + def to_dict(self) -> SelectOptionPayload: + payload: SelectOptionPayload = { + 'label': self.label, + 'value': self.value, + 'default': self.default, + } + + if self.emoji: + payload['emoji'] = self.emoji.to_dict() # type: ignore + + if self.description: + payload['description'] = self.description + + return payload + + def _component_factory(data: ComponentPayload) -> Component: component_type = data['type'] if component_type == 1: return ActionRow(data) elif component_type == 2: return Button(data) # type: ignore + elif component_type == 3: + return SelectMenu(data) # type: ignore else: as_enum = try_enum(ComponentType, component_type) return Component._raw_construct(type=as_enum) diff --git a/discord/enums.py b/discord/enums.py index cd6b54b07..fa86767db 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -458,6 +458,7 @@ class VideoQualityMode(Enum): class ComponentType(Enum): action_row = 1 button = 2 + select = 3 def __int__(self): return self.value diff --git a/discord/types/components.py b/discord/types/components.py index 8f1838b87..551a97ace 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -27,7 +27,7 @@ from __future__ import annotations from typing import List, Literal, TypedDict, Union from .emoji import PartialEmoji -ComponentType = Literal[1, 2] +ComponentType = Literal[1, 2, 3] ButtonStyle = Literal[1, 2, 3, 4, 5] @@ -43,9 +43,33 @@ class _ButtonComponentOptional(TypedDict, total=False): emoji: PartialEmoji label: str + class ButtonComponent(_ButtonComponentOptional): type: Literal[2] style: ButtonStyle -Component = Union[ActionRow, ButtonComponent] +class _SelectMenuOptional(TypedDict, total=False): + placeholder: str + min_values: int + max_values: int + + +class _SelectOptionsOptional(TypedDict, total=False): + description: str + emoji: PartialEmoji + + +class SelectOption(_SelectOptionsOptional): + label: str + value: str + default: bool + + +class SelectMenu(_SelectMenuOptional): + type: Literal[3] + custom_id: str + options: List[SelectOption] + + +Component = Union[ActionRow, ButtonComponent, SelectMenu] diff --git a/docs/api.rst b/docs/api.rst index 5639b85e5..62764a23d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1224,6 +1224,10 @@ of :class:`enum.Enum`. .. attribute:: button Represents a button component. + .. attribute:: select + + Represents a select component. + .. class:: ButtonStyle @@ -2902,6 +2906,15 @@ Button :members: :inherited-members: +SelectMenu +~~~~~~~~~~~ + +.. attributetable:: SelectMenu + +.. autoclass:: SelectMenu() + :members: + :inherited-members: + DeletedReferencedMessage ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -3316,6 +3329,14 @@ PartialMessage .. autoclass:: PartialMessage :members: +SelectOption +~~~~~~~~~~~~~ + +.. attributetable:: SelectOption + +.. autoclass:: SelectOption + :members: + Intents ~~~~~~~~~~