diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d9b0b949..6a15e0de5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ These changes are available on the `master` branch, but have not yet been releas - Added `Member.colours` and `Member.colors` properties. ([#3063](https://github.com/Pycord-Development/pycord/pull/3063)) +- Added `RadioGroup`, `CheckboxGroup`, and `Checkbox` for modals. + ([#3073](https://github.com/Pycord-Development/pycord/pull/3073)) - Added the ability to respond to interactions with suppressed push and desktop notifications. ([#3062](https://github.com/Pycord-Development/pycord/pull/3062)) - Added `User.collectibles` property. diff --git a/discord/components.py b/discord/components.py index f2cb44bbcc..d6e1eb89a8 100644 --- a/discord/components.py +++ b/discord/components.py @@ -47,6 +47,11 @@ from .emoji import AppEmoji, GuildEmoji from .types.components import ActionRow as ActionRowPayload from .types.components import ButtonComponent as ButtonComponentPayload + from .types.components import CheckboxComponent as CheckboxComponentPayload + from .types.components import ( + CheckboxGroupComponent as CheckboxGroupComponentPayload, + ) + from .types.components import CheckboxGroupOption as CheckboxGroupOptionPayload from .types.components import Component as ComponentPayload from .types.components import ContainerComponent as ContainerComponentPayload from .types.components import FileComponent as FileComponentPayload @@ -55,6 +60,8 @@ from .types.components import LabelComponent as LabelComponentPayload from .types.components import MediaGalleryComponent as MediaGalleryComponentPayload from .types.components import MediaGalleryItem as MediaGalleryItemPayload + from .types.components import RadioGroupComponent as RadioGroupComponentPayload + from .types.components import RadioGroupOption as RadioGroupOptionPayload from .types.components import SectionComponent as SectionComponentPayload from .types.components import SelectDefaultValue as SelectDefaultValuePayload from .types.components import SelectMenu as SelectMenuPayload @@ -83,6 +90,11 @@ "Label", "SelectDefaultValue", "FileUpload", + "RadioGroup", + "RadioGroupOption", + "CheckboxGroup", + "CheckboxGroupOption", + "Checkbox", ) C = TypeVar("C", bound="Component") @@ -1331,7 +1343,7 @@ class Label(Component): __slots__: tuple[str, ...] = ("component", "label", "description") __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = () + versions: tuple[int, ...] = (2,) def __init__(self, data: LabelComponentPayload): self.type: ComponentType = try_enum(ComponentType, data["type"]) @@ -1389,7 +1401,7 @@ class FileUpload(Component): ) __repr_info__: ClassVar[tuple[str, ...]] = __slots__ - versions: tuple[int, ...] = (1, 2) + versions: tuple[int, ...] = (2,) def __init__(self, data: FileUploadComponentPayload): self.type = ComponentType.file_upload @@ -1419,6 +1431,361 @@ def to_dict(self) -> FileUploadComponentPayload: return payload # type: ignore +class RadioGroup(Component): + """Represents an Radio Group component from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.RadioGroup` instead. + + .. versionadded:: 2.8 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The custom ID of the radio group that gets received during an interaction. + options: List[:class:`RadioGroupOption`] + A list of options that can be selected in this group, between 2 and 10. + required: Optional[:class:`bool`] + Whether the radio group requires a selection or not. Defaults to ``True``. + id: Optional[:class:`int`] + The radio group's ID. + """ + + __slots__: tuple[str, ...] = ( + "type", + "custom_id", + "options", + "required", + "id", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: RadioGroupComponentPayload): + self.type = ComponentType.radio_group + self.id: int | None = data.get("id") + self.custom_id = data.get("custom_id") + self.options: list[RadioGroupOption] = [ + RadioGroupOption.from_dict(option) for option in data.get("options", []) + ] + self.required: bool = data.get("required", True) + + def to_dict(self) -> RadioGroupComponentPayload: + payload = { + "type": 21, + "custom_id": self.custom_id, + "options": [opt.to_dict() for opt in self.options], + } + if self.id is not None: + payload["id"] = self.id + + if not self.required: + payload["required"] = self.required + + return payload + + +class RadioGroupOption: + """Represents a :class:`discord.RadioGroup`'s option. + + These can be created by users. + + .. versionadded:: 2.8 + + Attributes + ---------- + label: :class:`str` + The label of the option. This is displayed to users. + Can only be up to 100 characters. + value: :class:`str` + The value of the option. This is not displayed to users. + If not provided when constructed then it defaults to the + label. Can only be up to 100 characters. + description: Optional[:class:`str`] + An additional description of the option, if any. + Can only be up to 100 characters. + default: :class:`bool` + Whether this option is selected by default. Only 1 option should be set to default within a :class:`discord.RadioGroup`. + """ + + __slots__: tuple[str, ...] = ( + "label", + "value", + "description", + "default", + ) + + def __init__( + self, + *, + label: str, + value: str = MISSING, + description: str | None = None, + default: bool = False, + ) -> None: + if len(label) > 100: + raise ValueError("label must be 100 characters or fewer") + + if value is not MISSING and len(value) > 100: + raise ValueError("value must be 100 characters or fewer") + + if description is not None and len(description) > 100: + raise ValueError("description must be 100 characters or fewer") + + self.label = label + self.value = label if value is MISSING else value + self.description = description + self.default = default + + def __repr__(self) -> str: + return ( + "" + ) + + def __str__(self) -> str: + if self.description: + return f"{self.label}\n{self.description}" + return self.label + + @classmethod + def from_dict(cls, data: RadioGroupOptionPayload) -> RadioGroupOption: + return cls( + label=data["label"], + value=data["value"], + description=data.get("description"), + default=data.get("default", False), + ) + + def to_dict(self) -> RadioGroupOptionPayload: + payload: RadioGroupOptionPayload = { + "label": self.label, + "value": self.value, + "default": self.default, + } + + if self.description: + payload["description"] = self.description + + return payload + + +class CheckboxGroup(Component): + """Represents an Checkbox Group component from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.CheckboxGroup` instead. + + .. versionadded:: 2.8 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The custom ID of the checkbox group that gets received during an interaction. + options: List[:class:`CheckboxGroupOption`] + A list of options that can be selected in this group. + min_values: Optional[:class:`int`] + The minimum number of options that must be selected. + Defaults to 1 and must be between 0 and 25. If set to 0, :attr:`required` must be ``False``. + max_values: Optional[:class:`int`] + The maximum number of options that can be selected. + Must be between 1 and 10. + required: Optional[:class:`bool`] + Whether the checkbox group requires a selection or not. Defaults to ``True``. + id: Optional[:class:`int`] + The checkbox group's ID. + """ + + __slots__: tuple[str, ...] = ( + "type", + "custom_id", + "options", + "min_values", + "max_values", + "required", + "id", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: CheckboxGroupComponentPayload): + self.type = ComponentType.checkbox_group + self.id: int | None = data.get("id") + self.custom_id = data.get("custom_id") + self.options: list[CheckboxGroupOption] = [ + CheckboxGroupOption.from_dict(option) for option in data.get("options", []) + ] + self.min_values: int | None = data.get("min_values", None) + self.max_values: int | None = data.get("max_values", None) + self.required: bool = data.get("required", True) + + def to_dict(self) -> CheckboxGroupComponentPayload: + payload = { + "type": 22, + "custom_id": self.custom_id, + "options": [opt.to_dict() for opt in self.options], + } + if self.id is not None: + payload["id"] = self.id + + if self.min_values is not None: + payload["min_values"] = self.min_values + + if self.max_values is not None: + payload["max_values"] = self.max_values + + if not self.required: + payload["required"] = self.required + + return payload + + +class CheckboxGroupOption: + """Represents a :class:`discord.CheckboxGroup`'s option. + + These can be created by users. + + .. versionadded:: 2.8 + + Attributes + ---------- + label: :class:`str` + The label of the option. This is displayed to users. + Can only be up to 100 characters. + value: :class:`str` + The value of the option. This is not displayed to users. + If not provided when constructed then it defaults to the + label. Can only be up to 100 characters. + description: Optional[:class:`str`] + An additional description of the option, if any. + Can only be up to 100 characters. + default: :class:`bool` + Whether this option is selected by default. + """ + + __slots__: tuple[str, ...] = ( + "label", + "value", + "description", + "default", + ) + + def __init__( + self, + *, + label: str, + value: str = MISSING, + description: str | None = None, + default: bool = False, + ) -> None: + if len(label) > 100: + raise ValueError("label must be 100 characters or fewer") + + if value is not MISSING and len(value) > 100: + raise ValueError("value must be 100 characters or fewer") + + if description is not None and len(description) > 100: + raise ValueError("description must be 100 characters or fewer") + + self.label = label + self.value = label if value is MISSING else value + self.description = description + self.default = default + + def __repr__(self) -> str: + return ( + "" + ) + + def __str__(self) -> str: + if self.description: + return f"{self.label}\n{self.description}" + return self.label + + @classmethod + def from_dict(cls, data: CheckboxGroupOptionPayload) -> CheckboxGroupOption: + return cls( + label=data["label"], + value=data["value"], + description=data.get("description"), + default=data.get("default", False), + ) + + def to_dict(self) -> CheckboxGroupOptionPayload: + payload: CheckboxGroupOptionPayload = { + "label": self.label, + "value": self.value, + "default": self.default, + } + + if self.description: + payload["description"] = self.description + + return payload + + +class Checkbox(Component): + """Represents an Checkbox component from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.Checkbox` instead. + + .. versionadded:: 2.8 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The custom ID of the checkbox group that gets received during an interaction. + default: Optional[:class:`bool`] + Whether this checkbox is selected by default. + id: Optional[:class:`int`] + The checkbox group's ID. + """ + + __slots__: tuple[str, ...] = ( + "type", + "custom_id", + "default", + "id", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: CheckboxComponentPayload): + self.type = ComponentType.checkbox + self.id: int | None = data.get("id") + self.custom_id = data.get("custom_id") + self.default: bool = data.get("default", False) + + def to_dict(self) -> CheckboxComponentPayload: + payload = { + "type": 23, + "custom_id": self.custom_id, + } + if self.id is not None: + payload["id"] = self.id + + if self.default is not None: + payload["default"] = self.default + + return payload + + COMPONENT_MAPPINGS = { 1: ActionRow, 2: Button, @@ -1437,6 +1804,9 @@ def to_dict(self) -> FileUploadComponentPayload: 17: Container, 18: Label, 19: FileUpload, + 21: RadioGroup, + 22: CheckboxGroup, + 23: Checkbox, } STATE_COMPONENTS = (Section, Container, Thumbnail, MediaGallery, FileComponent) diff --git a/discord/enums.py b/discord/enums.py index d561513040..fd5b9c416e 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -735,6 +735,9 @@ class ComponentType(Enum): container = 17 label = 18 file_upload = 19 + radio_group = 21 + checkbox_group = 22 + checkbox = 23 def __int__(self): return self.value diff --git a/discord/types/components.py b/discord/types/components.py index 0e862d6ab6..af567b6566 100644 --- a/discord/types/components.py +++ b/discord/types/components.py @@ -33,7 +33,9 @@ from .emoji import PartialEmoji from .snowflake import Snowflake -ComponentType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19] +ComponentType = Literal[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 21, 22, 23 +] ButtonStyle = Literal[1, 2, 3, 4, 5, 6] InputTextStyle = Literal[1, 2] SeparatorSpacingSize = Literal[1, 2] @@ -47,7 +49,7 @@ class BaseComponent(TypedDict): class ActionRow(BaseComponent): type: Literal[1] - components: list[ButtonComponent | InputText | SelectMenu] + components: list[AllowedActionRowComponents] class ButtonComponent(BaseComponent): @@ -106,8 +108,8 @@ class TextDisplayComponent(BaseComponent): class SectionComponent(BaseComponent): type: Literal[9] - components: list[TextDisplayComponent] - accessory: NotRequired[ThumbnailComponent | ButtonComponent] + components: list[AllowedSectionComponents] + accessory: NotRequired[AllowedSectionAccessories] class UnfurledMediaItem(TypedDict): @@ -163,7 +165,7 @@ class LabelComponent(BaseComponent): type: Literal[18] label: str description: NotRequired[str] - component: SelectMenu | InputText | FileUploadComponent + component: AllowedLabelComponents class FileUploadComponent(BaseComponent): @@ -174,10 +176,66 @@ class FileUploadComponent(BaseComponent): required: NotRequired[bool] +class RadioGroupOption(TypedDict): + value: str + label: str + description: NotRequired[str] + default: NotRequired[bool] + + +class RadioGroupComponent(BaseComponent): + type: Literal[21] + custom_id: str + required: NotRequired[bool] + options: list[RadioGroupOption] + + +class CheckboxGroupOption(TypedDict): + value: str + label: str + description: NotRequired[str] + default: NotRequired[bool] + + +class CheckboxGroupComponent(BaseComponent): + type: Literal[22] + custom_id: str + required: NotRequired[bool] + options: list[CheckboxGroupOption] + max_values: NotRequired[int] + min_values: NotRequired[int] + + +class CheckboxComponent(BaseComponent): + type: Literal[23] + custom_id: str + default: NotRequired[bool] + + Component = Union[ - ActionRow, ButtonComponent, SelectMenu, InputText, FileUploadComponent + ActionRow, + ButtonComponent, + SelectMenu, + InputText, + TextDisplayComponent, + SectionComponent, + ThumbnailComponent, + MediaGalleryComponent, + FileComponent, + SeparatorComponent, + ContainerComponent, + LabelComponent, + FileUploadComponent, + RadioGroupComponent, + CheckboxGroupComponent, + CheckboxComponent, ] +AllowedActionRowComponents = Union[ButtonComponent, InputText, SelectMenu] + +AllowedSectionAccessories = Union[ThumbnailComponent, ButtonComponent] + +AllowedSectionComponents = Union[TextDisplayComponent] AllowedContainerComponents = Union[ ActionRow, @@ -187,3 +245,12 @@ class FileUploadComponent(BaseComponent): SeparatorComponent, SectionComponent, ] + +AllowedLabelComponents = Union[ + SelectMenu, + InputText, + FileUploadComponent, + RadioGroupComponent, + CheckboxComponent, + CheckboxGroupComponent, +] diff --git a/discord/ui/__init__.py b/discord/ui/__init__.py index e0260952dc..e4044b00ab 100644 --- a/discord/ui/__init__.py +++ b/discord/ui/__init__.py @@ -10,6 +10,8 @@ from .action_row import * from .button import * +from .checkbox import * +from .checkbox_group import * from .container import * from .core import * from .file import * @@ -19,6 +21,7 @@ from .label import * from .media_gallery import * from .modal import * +from .radio_group import * from .section import * from .select import * from .separator import * diff --git a/discord/ui/checkbox.py b/discord/ui/checkbox.py new file mode 100644 index 0000000000..25d11ef644 --- /dev/null +++ b/discord/ui/checkbox.py @@ -0,0 +1,137 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from ..components import Checkbox as CheckboxComponent +from ..enums import ComponentType +from .item import ModalItem + +__all__ = ("Checkbox",) + +if TYPE_CHECKING: + from ..interactions import Interaction + from ..types.components import CheckboxComponent as CheckboxComponentPayload + + +class Checkbox(ModalItem): + """Represents a UI Checkbox component. + + .. versionadded:: 2.8 + + Parameters + ---------- + custom_id: Optional[:class:`str`] + The ID of the checkbox that gets received during an interaction. + default: Optional[:class:`bool`] + Whether this checkbox is selected by default or not. + id: Optional[:class:`int`] + The checkbox's ID. + """ + + __item_repr_attributes__: tuple[str, ...] = ( + "default", + "custom_id", + "id", + ) + + def __init__( + self, + *, + custom_id: str | None = None, + default: bool = False, + id: int | None = None, + ): + super().__init__() + if custom_id is not None and not isinstance(custom_id, str): + raise TypeError( + f"expected custom_id to be str, not {custom_id.__class__.__name__}" + ) + if not isinstance(default, bool): + raise TypeError(f"default must be bool, not {default.__class__.__name__}") + custom_id = os.urandom(16).hex() if custom_id is None else custom_id + self._value: bool | None = None + + self._underlying: CheckboxComponent = self._generate_underlying( + custom_id=custom_id, + default=default, + id=id, + ) + + def _generate_underlying( + self, + custom_id: str | None = None, + default: bool | None = None, + id: int | None = None, + ) -> CheckboxComponent: + super()._generate_underlying(CheckboxComponent) + return CheckboxComponent._raw_construct( + type=ComponentType.checkbox, + custom_id=custom_id or self.custom_id, + required=default if default is not None else self.default, + id=id or self.id, + ) + + @property + def custom_id(self) -> str: + """The custom id that gets received during an interaction.""" + return self.underlying.custom_id + + @custom_id.setter + def custom_id(self, value: str): + if not isinstance(value, str): + raise TypeError(f"custom_id must be str not {value.__class__.__name__}") + if len(value) > 100: + raise ValueError("custom_id must be 100 characters or fewer") + self.underlying.custom_id = value + + @property + def default(self) -> bool: + """Whether this checkbox is selected by default or not. Defaults to ``False``""" + return self.underlying.default + + @default.setter + def default(self, value: bool): + if not isinstance(value, bool): + raise TypeError(f"default must be bool, not {value.__class__.__name__}") + self.underlying.default = bool(value) + + @property + def value(self) -> bool | None: + """Whether this checkbox was selected or not by the user. This will be ``None`` if the checkbox has not been submitted via a modal yet.""" + return self._value + + def to_component_dict(self) -> CheckboxComponentPayload: + return self.underlying.to_dict() + + def refresh_state(self, data) -> None: + self._value = data.get("value", None) + + def refresh_from_modal( + self, interaction: Interaction, data: CheckboxComponentPayload + ) -> None: + return self.refresh_state(data) diff --git a/discord/ui/checkbox_group.py b/discord/ui/checkbox_group.py new file mode 100644 index 0000000000..ed155898ab --- /dev/null +++ b/discord/ui/checkbox_group.py @@ -0,0 +1,286 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from typing_extensions import Self + +from ..components import CheckboxGroup as CheckboxGroupComponent +from ..components import CheckboxGroupOption +from ..enums import ComponentType +from ..utils import MISSING +from .item import ModalItem + +__all__ = ("CheckboxGroup",) + +if TYPE_CHECKING: + from ..interactions import Interaction + from ..types.components import ( + CheckboxGroupComponent as CheckboxGroupComponentPayload, + ) + + +class CheckboxGroup(ModalItem): + """Represents a UI Checkbox Group component. + + .. versionadded:: 2.8 + + Parameters + ---------- + custom_id: Optional[:class:`str`] + The ID of the checkbox group that gets received during an interaction. + options: List[:class:`discord.CheckboxGroupOption`] + A list of options that can be selected in this group. + min_values: Optional[:class:`int`] + The minimum number of options that must be selected. + Defaults to 0 and must be between 0 and 10, inclusive. + max_values: Optional[:class:`int`] + The maximum number of options that can be selected. + Must be between 1 and 10, inclusive. + required: Optional[:class:`bool`] + Whether an option selection is required or not. Defaults to ``True``. + id: Optional[:class:`int`] + The checkbox group's ID. + """ + + __item_repr_attributes__: tuple[str, ...] = ( + "options", + "required", + "min_values", + "max_values", + "custom_id", + "id", + ) + + def __init__( + self, + *, + custom_id: str | None = None, + options: list[CheckboxGroupOption] | None = None, + min_values: int | None = None, + max_values: int | None = None, + required: bool = True, + id: int | None = None, + ): + super().__init__() + if min_values and (min_values < 0 or min_values > 10): + raise ValueError("min_values must be between 0 and 10") + if max_values and (max_values < 1 or max_values > 10): + raise ValueError("max_values must be between 1 and 10") + if custom_id is not None and not isinstance(custom_id, str): + raise TypeError( + f"expected custom_id to be str, not {custom_id.__class__.__name__}" + ) + if not isinstance(required, bool): + raise TypeError(f"required must be bool not {required.__class__.__name__}") + custom_id = os.urandom(16).hex() if custom_id is None else custom_id + self._selected_values: list[str] | None = None + + self._underlying: CheckboxGroupComponent = self._generate_underlying( + custom_id=custom_id, + options=[], + min_values=min_values, + max_values=max_values, + required=required, + id=id, + ) + if options: + self.options = options + + def _generate_underlying( + self, + custom_id: str | None = None, + options: list[CheckboxGroupOption] | None = None, + min_values: int | None = None, + max_values: int | None = None, + required: bool | None = None, + id: int | None = None, + ) -> CheckboxGroupComponent: + super()._generate_underlying(CheckboxGroupComponent) + return CheckboxGroupComponent._raw_construct( + type=ComponentType.checkbox_group, + custom_id=custom_id or self.custom_id, + options=options if options is not None else self.options, + min_values=min_values if min_values is not None else self.min_values, + max_values=max_values if max_values is not None else self.max_values, + required=required if required is not None else self.required, + id=id or self.id, + ) + + @property + def custom_id(self) -> str: + """The custom id that gets received during an interaction.""" + return self.underlying.custom_id + + @custom_id.setter + def custom_id(self, value: str): + if not isinstance(value, str): + raise TypeError(f"custom_id must be str not {value.__class__.__name__}") + if len(value) > 100: + raise ValueError("custom_id must be 100 characters or fewer") + self.underlying.custom_id = value + + @property + def min_values(self) -> int | None: + """The minimum number of options that must be selected.""" + return self.underlying.min_values + + @min_values.setter + def min_values(self, value: int | None): + if value and not isinstance(value, int): + raise TypeError( + f"min_values must be None or int not {value.__class__.__name__}" + ) + if value and (value < 0 or value > 10): + raise ValueError("min_values must be between 0 and 10") + self.underlying.min_values = value + + @property + def max_values(self) -> int | None: + """The maximum number of options that can be selected.""" + return self.underlying.max_values + + @max_values.setter + def max_values(self, value: int | None): + if value and not isinstance(value, int): + raise TypeError( + f"max_values must be None or int not {value.__class__.__name__}" + ) + if value and (value < 1 or value > 10): + raise ValueError("max_values must be between 1 and 10") + self.underlying.max_values = value + + @property + def required(self) -> bool: + """Whether an option selection is required or not. Defaults to ``True``""" + return self.underlying.required + + @required.setter + def required(self, value: bool): + if not isinstance(value, bool): + raise TypeError(f"required must be bool, not {value.__class__.__name__}") + self.underlying.required = bool(value) + + @property + def values(self) -> list[str] | None: + """The values selected by the user. This will be ``None`` if the checkbox group has not been submitted in a modal yet.""" + return self._selected_values + + @property + def options(self) -> list[CheckboxGroupOption]: + """A list of options that can be selected in this group.""" + return self.underlying.options + + @options.setter + def options(self, value: list[CheckboxGroupOption]): + if not isinstance(value, list): + raise TypeError("options must be a list of CheckboxGroupOption") + if len(value) > 10: + raise ValueError("you may only provide up to 10 options.") + if not all(isinstance(obj, CheckboxGroupOption) for obj in value): + raise TypeError("all list items must subclass CheckboxGroupOption") + + self.underlying.options = value + + def add_option( + self, + *, + label: str, + value: str = MISSING, + description: str | None = None, + default: bool = False, + ) -> Self: + """Adds an option to the checkbox group. + + To append a pre-existing :class:`discord.CheckboxGroupOption` use the + :meth:`append_option` method instead. + + Parameters + ---------- + label: :class:`str` + The label of the option. This is displayed to users. + Can only be up to 100 characters. + value: :class:`str` + The value of the option. This is not displayed to users. + If not given, defaults to the label. Can only be up to 100 characters. + description: Optional[:class:`str`] + An additional description of the option, if any. + Can only be up to 100 characters. + default: :class:`bool` + Whether this option is selected by default. + + Raises + ------ + ValueError + The number of options exceeds 10. + """ + + option = CheckboxGroupOption( + label=label, + value=value, + description=description, + default=default, + ) + + return self.append_option(option) + + def append_option(self, option: CheckboxGroupOption) -> Self: + """Appends an option to the checkbox group. + + Parameters + ---------- + option: :class:`discord.CheckboxGroupOption` + The option to append to the checkbox group. + + Raises + ------ + ValueError + The number of options exceeds 10. + """ + + if len(self.underlying.options) >= 10: + raise ValueError("maximum number of options already provided") + + self.underlying.options.append(option) + return self + + def clear_options(self) -> Self: + """Remove all options from the checkbox group.""" + + self.underlying.options.clear() + return self + + def to_component_dict(self) -> CheckboxGroupComponentPayload: + return self.underlying.to_dict() + + def refresh_state(self, data) -> None: + self._selected_values = data.get("values", []) + + def refresh_from_modal( + self, interaction: Interaction, data: CheckboxGroupComponentPayload + ) -> None: + return self.refresh_state(data) diff --git a/discord/ui/file_upload.py b/discord/ui/file_upload.py index f77a373b47..784b8f6733 100644 --- a/discord/ui/file_upload.py +++ b/discord/ui/file_upload.py @@ -1,3 +1,27 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + from __future__ import annotations import os @@ -23,7 +47,7 @@ class FileUpload(ModalItem): Parameters ---------- custom_id: Optional[:class:`str`] - The ID of the input text field that gets received during an interaction. + The ID of the file upload field that gets received during an interaction. min_values: Optional[:class:`int`] The minimum number of files that must be uploaded. Defaults to 0 and must be between 0 and 10, inclusive. @@ -57,7 +81,7 @@ def __init__( if min_values and (min_values < 0 or min_values > 10): raise ValueError("min_values must be between 0 and 10") if max_values and (max_values < 1 or max_values > 10): - raise ValueError("max_length must be between 1 and 10") + raise ValueError("max_values must be between 1 and 10") if custom_id is not None and not isinstance(custom_id, str): raise TypeError( f"expected custom_id to be str, not {custom_id.__class__.__name__}" @@ -75,12 +99,6 @@ def __init__( id=id, ) - def __repr__(self) -> str: - attrs = " ".join( - f"{key}={getattr(self, key)!r}" for key in self.__item_repr_attributes__ - ) - return f"<{self.__class__.__name__} {attrs}>" - def _generate_underlying( self, custom_id: str | None = None, @@ -99,15 +117,6 @@ def _generate_underlying( id=id or self.id, ) - @property - def type(self) -> ComponentType: - return self.underlying.type - - @property - def id(self) -> int | None: - """The ID of this component. If not provided by the user, it is set sequentially by Discord.""" - return self.underlying.id - @property def custom_id(self) -> str: """The custom id that gets received during an interaction.""" @@ -116,9 +125,9 @@ def custom_id(self) -> str: @custom_id.setter def custom_id(self, value: str): if not isinstance(value, str): - raise TypeError( - f"custom_id must be None or str not {value.__class__.__name__}" - ) + raise TypeError(f"custom_id must be str not {value.__class__.__name__}") + if len(value) > 100: + raise ValueError("custom_id must be 100 characters or fewer") self.underlying.custom_id = value @property @@ -160,7 +169,7 @@ def required(self, value: bool): @property def values(self) -> list[Attachment] | None: - """The files that were uploaded to the field.""" + """The files that were uploaded to the field. This will be ``None`` if the file upload has not been submitted via a modal yet.""" return self._attachments def to_component_dict(self) -> FileUploadComponentPayload: diff --git a/discord/ui/input_text.py b/discord/ui/input_text.py index 33fed96fbe..5b632e933c 100644 --- a/discord/ui/input_text.py +++ b/discord/ui/input_text.py @@ -132,12 +132,6 @@ def __init__( id=id, ) - def __repr__(self) -> str: - attrs = " ".join( - f"{key}={getattr(self, key)!r}" for key in self.__item_repr_attributes__ - ) - return f"<{self.__class__.__name__} {attrs}>" - def _generate_underlying( self, style: InputTextStyle | None = None, @@ -185,9 +179,9 @@ def custom_id(self) -> str: @custom_id.setter def custom_id(self, value: str): if not isinstance(value, str): - raise TypeError( - f"custom_id must be None or str not {value.__class__.__name__}" - ) + raise TypeError(f"custom_id must be str not {value.__class__.__name__}") + if len(value) > 100: + raise ValueError("custom_id must be 100 characters or fewer") self.underlying.custom_id = value @property diff --git a/discord/ui/item.py b/discord/ui/item.py index 5531ed67b6..85132558bd 100644 --- a/discord/ui/item.py +++ b/discord/ui/item.py @@ -92,6 +92,7 @@ def underlying(self, value: Component) -> None: @property def type(self) -> ComponentType: + """The underlying component's type.""" if not self.underlying: raise NotImplementedError return self.underlying.type @@ -231,10 +232,14 @@ class ModalItem(Item[M]): - :class:`discord.ui.Label` - :class:`discord.ui.TextDisplay` - And :class:`discord.ui.Label` should be used in :class:`discord.ui.DesignerModal` to support the following items: + And :class:`discord.ui.Label` should be used in :class:`discord.ui.DesignerModal` to add the following items: + - :class:`discord.ui.InputText` - :class:`discord.ui.Select` - :class:`discord.ui.FileUpload` + - :class:`discord.ui.RadioGroup` + - :class:`discord.ui.CheckboxGroup` + - :class:`discord.ui.Checkbox` .. versionadded:: 2.7 """ diff --git a/discord/ui/label.py b/discord/ui/label.py index 8c11c8dbeb..19fea37e17 100644 --- a/discord/ui/label.py +++ b/discord/ui/label.py @@ -27,14 +27,25 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Iterator, Literal, TypeVar, overload +from ..components import ( + CheckboxGroupOption, +) from ..components import Label as LabelComponent -from ..components import SelectDefaultValue, SelectOption, _component_factory +from ..components import ( + RadioGroupOption, + SelectDefaultValue, + SelectOption, + _component_factory, +) from ..enums import ButtonStyle, ChannelType, ComponentType, InputTextStyle from ..utils import find, get from .button import Button +from .checkbox import Checkbox +from .checkbox_group import CheckboxGroup from .file_upload import FileUpload from .input_text import InputText from .item import ItemCallbackType, ModalItem +from .radio_group import RadioGroup from .select import Select __all__ = ("Label",) @@ -61,6 +72,9 @@ class Label(ModalItem[M]): - :class:`discord.ui.Select` - :class:`discord.ui.InputText` - :class:`discord.ui.FileUpload` + - :class:`discord.ui.RadioGroup` + - :class:`discord.ui.CheckboxGroup` + - :class:`discord.ui.Checkbox` .. versionadded:: 2.7 @@ -391,6 +405,114 @@ def set_file_upload( return self.set_item(upload) + def set_radio_group( + self, + *, + custom_id: str | None = None, + options: list[RadioGroupOption] | None = None, + required: bool | None = True, + id: int | None = None, + ) -> Self: + """Set this label's item to a radio group. + + To set a pre-existing :class:`RadioGroup`, use the + :meth:`set_item` method, instead. + + Parameters + ---------- + custom_id: Optional[:class:`str`] + The ID of the radio group that gets received during an interaction. + options: List[:class:`discord.RadioGroupOption`] + A list of options that can be selected from this group. + required: Optional[:class:`bool`] + Whether an option selection is required or not. Defaults to ``True``. + id: Optional[:class:`int`] + The radio group's ID. + """ + + radio = RadioGroup( + custom_id=custom_id, + option=options, + required=required, + id=id, + ) + + return self.set_item(radio) + + def set_checkbox_group( + self, + *, + custom_id: str | None = None, + options: list[CheckboxGroupOption] | None = None, + min_values: int | None = None, + max_values: int | None = None, + required: bool | None = True, + id: int | None = None, + ) -> Self: + """Set this label's item to a checkbox group. + + To set a pre-existing :class:`CheckboxGroup`, use the + :meth:`set_item` method, instead. + + Parameters + ---------- + custom_id: Optional[:class:`str`] + The ID of the checkbox group that gets received during an interaction. + options: List[:class:`discord.CheckboxGroupOption`] + A list of options that can be selected in this group. + min_values: Optional[:class:`int`] + The minimum number of options that must be selected. + Defaults to 0 and must be between 0 and 10, inclusive. + max_values: Optional[:class:`int`] + The maximum number of options that can be selected. + Must be between 1 and 10, inclusive. + required: Optional[:class:`bool`] + Whether an option selection is required or not. Defaults to ``True``. + id: Optional[:class:`int`] + The checkbox group's ID. + """ + + checkboxes = CheckboxGroup( + custom_id=custom_id, + option=options, + min_values=min_values, + max_values=max_values, + required=required, + id=id, + ) + + return self.set_item(checkboxes) + + def set_checkbox( + self, + *, + custom_id: str | None = None, + default: bool | None = False, + id: int | None = None, + ) -> Self: + """Set this label's item to a checkbox. + + To set a pre-existing :class:`Checkbox`, use the + :meth:`set_item` method, instead. + + Parameters + ---------- + custom_id: Optional[:class:`str`] + The ID of the checkbox that gets received during an interaction. + default: Optional[:class:`bool`] + Whether this checkbox is selected by default or not. + id: Optional[:class:`int`] + The checkbox's ID. + """ + + checkbox = Checkbox( + custom_id=custom_id, + default=default, + id=id, + ) + + return self.set_item(checkbox) + @property def label(self) -> str: """The label text. Must be 45 characters or fewer.""" diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 1051156f80..6bb3a21d6a 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -35,7 +35,6 @@ from ..enums import ComponentType from ..utils import find from .core import ItemInterface -from .file_upload import FileUpload from .input_text import InputText from .item import ModalItem from .label import Label diff --git a/discord/ui/radio_group.py b/discord/ui/radio_group.py new file mode 100644 index 0000000000..7a01501776 --- /dev/null +++ b/discord/ui/radio_group.py @@ -0,0 +1,236 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from typing_extensions import Self + +from ..components import RadioGroup as RadioGroupComponent +from ..components import RadioGroupOption +from ..enums import ComponentType +from ..utils import MISSING +from .item import ModalItem + +__all__ = ("RadioGroup",) + +if TYPE_CHECKING: + from ..interactions import Interaction + from ..types.components import RadioGroupComponent as RadioGroupComponentPayload + + +class RadioGroup(ModalItem): + """Represents a UI Radio Group component. + + .. versionadded:: 2.8 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The ID of the radio group that gets received during an interaction. + options: List[:class:`discord.RadioGroupOption`] + A list of options that can be selected from this group. Must provide between 2 and 10 options. + required: Optional[:class:`bool`] + Whether an option selection is required or not. Defaults to ``True``. + id: Optional[:class:`int`] + The radio group's ID. + """ + + __item_repr_attributes__: tuple[str, ...] = ( + "options", + "required", + "custom_id", + "id", + ) + + def __init__( + self, + *, + custom_id: str | None = None, + options: list[RadioGroupOption] | None = None, + required: bool = True, + id: int | None = None, + ): + super().__init__() + if custom_id is not None and not isinstance(custom_id, str): + raise TypeError( + f"expected custom_id to be str, not {custom_id.__class__.__name__}" + ) + if not isinstance(required, bool): + raise TypeError(f"required must be bool not {required.__class__.__name__}") + custom_id = os.urandom(16).hex() if custom_id is None else custom_id + self._selected_value: str | None = None + + self._underlying: RadioGroupComponent = self._generate_underlying( + custom_id=custom_id, + options=[], + required=required, + id=id, + ) + if options: + self.options = options + + def _generate_underlying( + self, + custom_id: str | None = None, + options: list[RadioGroupOption] | None = None, + required: bool | None = None, + id: int | None = None, + ) -> RadioGroupComponent: + super()._generate_underlying(RadioGroupComponent) + return RadioGroupComponent._raw_construct( + type=ComponentType.radio_group, + custom_id=custom_id or self.custom_id, + options=options if options is not None else self.options, + required=required if required is not None else self.required, + id=id or self.id, + ) + + @property + def custom_id(self) -> str: + """The custom id that gets received during an interaction.""" + return self.underlying.custom_id + + @custom_id.setter + def custom_id(self, value: str): + if not isinstance(value, str): + raise TypeError(f"custom_id must be str not {value.__class__.__name__}") + if len(value) > 100: + raise ValueError("custom_id must be 100 characters or fewer") + self.underlying.custom_id = value + + @property + def required(self) -> bool: + """Whether an option selection is required or not. Defaults to ``True``""" + return self.underlying.required + + @required.setter + def required(self, value: bool): + if not isinstance(value, bool): + raise TypeError(f"required must be bool, not {value.__class__.__name__}") + self.underlying.required = bool(value) + + @property + def value(self) -> str | None: + """The value selected by the user. May return ``None`` if this radio group is optional or has not been sent yet.""" + return self._selected_value + + @property + def options(self) -> list[RadioGroupOption]: + """A list of options that can be selected in this group.""" + return self.underlying.options + + @options.setter + def options(self, value: list[RadioGroupOption]): + if not isinstance(value, list): + raise TypeError("options must be a list of RadioGroupOption") + if not (2 <= len(value) <= 10) and len(value) != 0: + raise ValueError("you must provide between 2 and 10 options.") + if not all(isinstance(obj, RadioGroupOption) for obj in value): + raise TypeError("all list items must subclass RadioGroupOption") + if len([o for o in value if o.default]) > 1: + raise ValueError("only 1 option can be set as default.") + + self.underlying.options = value + + def add_option( + self, + *, + label: str, + value: str = MISSING, + description: str | None = None, + default: bool = False, + ) -> Self: + """Adds an option to the radio group. + + To append a pre-existing :class:`discord.RadioGroupOption` use the + :meth:`append_option` method instead. + + Parameters + ---------- + label: :class:`str` + The label of the option. This is displayed to users. + Can only be up to 100 characters. + value: :class:`str` + The value of the option. This is not displayed to users. + If not given, defaults to the label. Can only be up to 100 characters. + description: Optional[:class:`str`] + An additional description of the option, if any. + Can only be up to 100 characters. + default: :class:`bool` + Whether this option is selected by default. + + Raises + ------ + ValueError + The number of options exceeds 10. + """ + + option = RadioGroupOption( + label=label, + value=value, + description=description, + default=default, + ) + + return self.append_option(option) + + def append_option(self, option: RadioGroupOption) -> Self: + """Appends an option to the radio group. + + Parameters + ---------- + option: :class:`discord.RadioGroupOption` + The option to append to the radio group. + + Raises + ------ + ValueError + The number of options exceeds 10. + """ + + if len(self.underlying.options) >= 10: + raise ValueError("maximum number of options already provided") + + self.underlying.options.append(option) + return self + + def clear_options(self) -> Self: + """Remove all options from the radio group.""" + + self.underlying.options.clear() + return self + + def to_component_dict(self) -> RadioGroupComponentPayload: + return self.underlying.to_dict() + + def refresh_state(self, data) -> None: + self._selected_value = data.get("value", None) + + def refresh_from_modal( + self, interaction: Interaction, data: RadioGroupComponentPayload + ) -> None: + return self.refresh_state(data) diff --git a/discord/ui/select.py b/discord/ui/select.py index 7045de7d97..d6f5a105ca 100644 --- a/discord/ui/select.py +++ b/discord/ui/select.py @@ -661,14 +661,14 @@ def append_option(self, option: SelectOption) -> Self: return self @property - def values(self) -> list[ST]: + def values(self) -> list[ST] | None: """List[:class:`str`] | List[:class:`discord.Member` | :class:`discord.User`]] | List[:class:`discord.Role`]] | List[:class:`discord.Member` | :class:`discord.User` | :class:`discord.Role`]] | List[:class:`discord.abc.GuildChannel`] | None: A list of values that have been selected by the user. This will be ``None`` if the select has not been interacted with yet. """ if self._interaction is None or self._interaction.data is None: # The select has not been interacted with yet - return [] + return None select_type = self.underlying.type if select_type is ComponentType.string_select: return self._selected_values # type: ignore # ST is str diff --git a/discord/ui/view.py b/discord/ui/view.py index 870cfec72f..281df6fb47 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -45,6 +45,8 @@ from ..components import ActionRow as ActionRowComponent from ..components import Button as ButtonComponent +from ..components import Checkbox as CheckboxComponent +from ..components import CheckboxGroup as CheckboxGroupComponent from ..components import Component from ..components import Container as ContainerComponent from ..components import FileComponent @@ -52,6 +54,7 @@ from ..components import InputText as InputTextComponent from ..components import Label as LabelComponent from ..components import MediaGallery as MediaGalleryComponent +from ..components import RadioGroup as RadioGroupComponent from ..components import Section as SectionComponent from ..components import SelectMenu as SelectComponent from ..components import Separator as SeparatorComponent @@ -62,7 +65,7 @@ from ..errors import Forbidden, NotFound from ..utils import find from .core import ItemInterface -from .item import ItemCallbackType, ViewItem +from .item import Item, ItemCallbackType, ModalItem, ViewItem __all__ = ( "BaseView", @@ -101,7 +104,8 @@ def _walk_all_components_v2(components: list[Component]) -> Iterator[Component]: yield item -def _component_to_item(component: Component) -> ViewItem[V]: +def _component_to_item(component: Component) -> ViewItem[V] | ModalItem | Item: + if isinstance(component, ButtonComponent): from .button import Button @@ -154,7 +158,19 @@ def _component_to_item(component: Component) -> ViewItem[V]: from .file_upload import FileUpload return FileUpload.from_component(component) - return ViewItem.from_component(component) + if isinstance(component, RadioGroupComponent): + from .radio_group import RadioGroup + + return RadioGroup.from_component(component) + if isinstance(component, CheckboxGroupComponent): + from .checkbox_group import CheckboxGroup + + return CheckboxGroup.from_component(component) + if isinstance(component, CheckboxComponent): + from .checkbox import Checkbox + + return Checkbox.from_component(component) + return Item.from_component(component) class _ViewWeights: diff --git a/docs/api/data_classes.rst b/docs/api/data_classes.rst index 87f86f1732..a28cb2a726 100644 --- a/docs/api/data_classes.rst +++ b/docs/api/data_classes.rst @@ -41,6 +41,16 @@ dynamic attributes in mind. .. autoclass:: UnfurledMediaItem :members: +.. attributetable:: RadioGroupOption + +.. autoclass:: RadioGroupOption + :members: + +.. attributetable:: CheckboxGroupOption + +.. autoclass:: CheckboxGroupOption + :members: + .. attributetable:: Intents .. autoclass:: Intents diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 428e04e790..3607ceeda5 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -535,6 +535,15 @@ of :class:`enum.Enum`. .. attribute:: file_upload Represents a file upload component. + .. attribute:: radio_group + + Represents a radio group component. + .. attribute:: checkbox_group + + Represents a checkbox group component. + .. attribute:: checkbox + + Represents a checkbox component. .. class:: ButtonStyle diff --git a/docs/api/models.rst b/docs/api/models.rst index 96d2ec19c5..d1c64e12bd 100644 --- a/docs/api/models.rst +++ b/docs/api/models.rst @@ -476,6 +476,24 @@ UI Components :members: :inherited-members: +.. attributetable:: RadioGroup + +.. autoclass:: RadioGroup() + :members: + :inherited-members: + +.. attributetable:: CheckboxGroup + +.. autoclass:: CheckboxGroup() + :members: + :inherited-members: + +.. attributetable:: Checkbox + +.. autoclass:: Checkbox() + :members: + :inherited-members: + Emoji ----- diff --git a/docs/api/ui_kit.rst b/docs/api/ui_kit.rst index 5963c2c4c9..3f02c18c10 100644 --- a/docs/api/ui_kit.rst +++ b/docs/api/ui_kit.rst @@ -183,3 +183,21 @@ Objects .. autoclass:: discord.ui.FileUpload :members: :inherited-members: + +.. attributetable:: discord.ui.RadioGroup + +.. autoclass:: discord.ui.RadioGroup + :members: + :inherited-members: + +.. attributetable:: discord.ui.CheckboxGroup + +.. autoclass:: discord.ui.CheckboxGroup + :members: + :inherited-members: + +.. attributetable:: discord.ui.Checkbox + +.. autoclass:: discord.ui.Checkbox + :members: + :inherited-members: