From bc91080ecae587226d6dc318e891452e258fa730 Mon Sep 17 00:00:00 2001 From: itsMapleLeaf <19603573+itsMapleLeaf@users.noreply.github.com> Date: Fri, 22 Jul 2022 21:28:14 -0500 Subject: [PATCH] allow JSX for text in more places --- .../core/components/button-shared-props.ts | 4 +- .../library/core/components/button.tsx | 16 +++- .../library/core/components/embed-author.tsx | 19 ++-- .../library/core/components/embed-field.tsx | 27 ++++-- .../library/core/components/embed-footer.tsx | 25 ++++-- .../library/core/components/embed-title.tsx | 21 +++-- .../reacord/library/core/components/link.tsx | 16 +++- .../library/core/components/option-node.ts | 11 ++- .../library/core/components/option.tsx | 36 ++++++-- .../reacord/library/internal/container.ts | 10 +++ packages/reacord/library/internal/node.ts | 4 + .../reacord/library/internal/text-node.ts | 4 + .../reacord/scripts/discordjs-manual-test.tsx | 6 +- packages/reacord/test/text-children.test.tsx | 89 +++++++++++++++++++ 14 files changed, 236 insertions(+), 52 deletions(-) create mode 100644 packages/reacord/test/text-children.test.tsx diff --git a/packages/reacord/library/core/components/button-shared-props.ts b/packages/reacord/library/core/components/button-shared-props.ts index 3cef1a7..4218af1 100644 --- a/packages/reacord/library/core/components/button-shared-props.ts +++ b/packages/reacord/library/core/components/button-shared-props.ts @@ -1,10 +1,12 @@ +import type { ReactNode } from "react" + /** * Common props between button-like components * @category Button */ export type ButtonSharedProps = { /** The text on the button. Rich formatting (markdown) is not supported here. */ - label?: string + label?: ReactNode /** When true, the button will be slightly faded, and cannot be clicked. */ disabled?: boolean diff --git a/packages/reacord/library/core/components/button.tsx b/packages/reacord/library/core/components/button.tsx index 02bf400..6f50f27 100644 --- a/packages/reacord/library/core/components/button.tsx +++ b/packages/reacord/library/core/components/button.tsx @@ -34,13 +34,23 @@ export type ButtonClickEvent = ComponentEvent */ export function Button(props: ButtonProps) { return ( - new ButtonNode(props)} /> + new ButtonNode(props)}> + new ButtonLabelNode({})}> + {props.label} + + ) } class ButtonNode extends Node { private customId = nanoid() + // this has text children, but buttons themselves shouldn't yield text + // eslint-disable-next-line class-methods-use-this + override get text() { + return "" + } + override modifyMessageOptions(options: MessageOptions): void { getNextActionRow(options).push({ type: "button", @@ -48,7 +58,7 @@ class ButtonNode extends Node { style: this.props.style ?? "secondary", disabled: this.props.disabled, emoji: this.props.emoji, - label: this.props.label, + label: this.children.findType(ButtonLabelNode)?.text, }) } @@ -63,3 +73,5 @@ class ButtonNode extends Node { return false } } + +class ButtonLabelNode extends Node<{}> {} diff --git a/packages/reacord/library/core/components/embed-author.tsx b/packages/reacord/library/core/components/embed-author.tsx index ae4609a..5009ca8 100644 --- a/packages/reacord/library/core/components/embed-author.tsx +++ b/packages/reacord/library/core/components/embed-author.tsx @@ -1,5 +1,7 @@ +import type { ReactNode } from "react" import React from "react" import { ReacordElement } from "../../internal/element.js" +import { Node } from "../../internal/node.js" import { EmbedChildNode } from "./embed-child.js" import type { EmbedOptions } from "./embed-options" @@ -7,8 +9,8 @@ import type { EmbedOptions } from "./embed-options" * @category Embed */ export type EmbedAuthorProps = { - name?: string - children?: string + name?: ReactNode + children?: ReactNode url?: string iconUrl?: string } @@ -18,19 +20,22 @@ export type EmbedAuthorProps = { */ export function EmbedAuthor(props: EmbedAuthorProps) { return ( - new EmbedAuthorNode(props)} - /> + new EmbedAuthorNode(props)}> + new AuthorTextNode({})}> + {props.name ?? props.children} + + ) } class EmbedAuthorNode extends EmbedChildNode { override modifyEmbedOptions(options: EmbedOptions): void { options.author = { - name: this.props.name ?? this.props.children ?? "", + name: this.children.findType(AuthorTextNode)?.text ?? "", url: this.props.url, icon_url: this.props.iconUrl, } } } + +class AuthorTextNode extends Node<{}> {} diff --git a/packages/reacord/library/core/components/embed-field.tsx b/packages/reacord/library/core/components/embed-field.tsx index 9c8fdf8..0ae0b23 100644 --- a/packages/reacord/library/core/components/embed-field.tsx +++ b/packages/reacord/library/core/components/embed-field.tsx @@ -1,5 +1,7 @@ +import type { ReactNode } from "react" import React from "react" import { ReacordElement } from "../../internal/element.js" +import { Node } from "../../internal/node.js" import { EmbedChildNode } from "./embed-child.js" import type { EmbedOptions } from "./embed-options" @@ -7,10 +9,10 @@ import type { EmbedOptions } from "./embed-options" * @category Embed */ export type EmbedFieldProps = { - name: string - value?: string + name: ReactNode + value?: ReactNode inline?: boolean - children?: string + children?: ReactNode } /** @@ -18,10 +20,14 @@ export type EmbedFieldProps = { */ export function EmbedField(props: EmbedFieldProps) { return ( - new EmbedFieldNode(props)} - /> + new EmbedFieldNode(props)}> + new FieldNameNode({})}> + {props.name} + + new FieldValueNode({})}> + {props.value || props.children} + + ) } @@ -29,9 +35,12 @@ class EmbedFieldNode extends EmbedChildNode { override modifyEmbedOptions(options: EmbedOptions): void { options.fields ??= [] options.fields.push({ - name: this.props.name, - value: this.props.value ?? this.props.children ?? "", + name: this.children.findType(FieldNameNode)?.text ?? "", + value: this.children.findType(FieldValueNode)?.text ?? "", inline: this.props.inline, }) } } + +class FieldNameNode extends Node<{}> {} +class FieldValueNode extends Node<{}> {} diff --git a/packages/reacord/library/core/components/embed-footer.tsx b/packages/reacord/library/core/components/embed-footer.tsx index 41e95ca..9340592 100644 --- a/packages/reacord/library/core/components/embed-footer.tsx +++ b/packages/reacord/library/core/components/embed-footer.tsx @@ -1,5 +1,7 @@ +import type { ReactNode } from "react" import React from "react" import { ReacordElement } from "../../internal/element.js" +import { Node } from "../../internal/node.js" import { EmbedChildNode } from "./embed-child.js" import type { EmbedOptions } from "./embed-options" @@ -7,8 +9,8 @@ import type { EmbedOptions } from "./embed-options" * @category Embed */ export type EmbedFooterProps = { - text?: string - children?: string + text?: ReactNode + children?: ReactNode iconUrl?: string timestamp?: string | number | Date } @@ -16,19 +18,22 @@ export type EmbedFooterProps = { /** * @category Embed */ -export function EmbedFooter(props: EmbedFooterProps) { +export function EmbedFooter({ text, children, ...props }: EmbedFooterProps) { return ( - new EmbedFooterNode(props)} - /> + new EmbedFooterNode(props)}> + new FooterTextNode({})}> + {text ?? children} + + ) } -class EmbedFooterNode extends EmbedChildNode { +class EmbedFooterNode extends EmbedChildNode< + Omit +> { override modifyEmbedOptions(options: EmbedOptions): void { options.footer = { - text: this.props.text ?? this.props.children ?? "", + text: this.children.findType(FooterTextNode)?.text ?? "", icon_url: this.props.iconUrl, } options.timestamp = this.props.timestamp @@ -36,3 +41,5 @@ class EmbedFooterNode extends EmbedChildNode { : undefined } } + +class FooterTextNode extends Node<{}> {} diff --git a/packages/reacord/library/core/components/embed-title.tsx b/packages/reacord/library/core/components/embed-title.tsx index 65ac052..10cb027 100644 --- a/packages/reacord/library/core/components/embed-title.tsx +++ b/packages/reacord/library/core/components/embed-title.tsx @@ -1,5 +1,7 @@ +import type { ReactNode } from "react" import React from "react" import { ReacordElement } from "../../internal/element.js" +import { Node } from "../../internal/node.js" import { EmbedChildNode } from "./embed-child.js" import type { EmbedOptions } from "./embed-options" @@ -7,25 +9,28 @@ import type { EmbedOptions } from "./embed-options" * @category Embed */ export type EmbedTitleProps = { - children: string + children: ReactNode url?: string } /** * @category Embed */ -export function EmbedTitle(props: EmbedTitleProps) { +export function EmbedTitle({ children, ...props }: EmbedTitleProps) { return ( - new EmbedTitleNode(props)} - /> + new EmbedTitleNode(props)}> + new TitleTextNode({})}> + {children} + + ) } -class EmbedTitleNode extends EmbedChildNode { +class EmbedTitleNode extends EmbedChildNode> { override modifyEmbedOptions(options: EmbedOptions): void { - options.title = this.props.children + options.title = this.children.findType(TitleTextNode)?.text ?? "" options.url = this.props.url } } + +class TitleTextNode extends Node<{}> {} diff --git a/packages/reacord/library/core/components/link.tsx b/packages/reacord/library/core/components/link.tsx index d3315be..990f75f 100644 --- a/packages/reacord/library/core/components/link.tsx +++ b/packages/reacord/library/core/components/link.tsx @@ -18,18 +18,26 @@ export type LinkProps = ButtonSharedProps & { /** * @category Link */ -export function Link(props: LinkProps) { - return new LinkNode(props)} /> +export function Link({ label, children, ...props }: LinkProps) { + return ( + new LinkNode(props)}> + new LinkTextNode({})}> + {label || children} + + + ) } -class LinkNode extends Node { +class LinkNode extends Node> { override modifyMessageOptions(options: MessageOptions): void { getNextActionRow(options).push({ type: "link", disabled: this.props.disabled, emoji: this.props.emoji, - label: this.props.label || this.props.children, + label: this.children.findType(LinkTextNode)?.text, url: this.props.url, }) } } + +class LinkTextNode extends Node<{}> {} diff --git a/packages/reacord/library/core/components/option-node.ts b/packages/reacord/library/core/components/option-node.ts index 4090b9e..067aeec 100644 --- a/packages/reacord/library/core/components/option-node.ts +++ b/packages/reacord/library/core/components/option-node.ts @@ -2,13 +2,18 @@ import type { MessageSelectOptionOptions } from "../../internal/message" import { Node } from "../../internal/node" import type { OptionProps } from "./option" -export class OptionNode extends Node { +export class OptionNode extends Node< + Omit +> { get options(): MessageSelectOptionOptions { return { - label: this.props.children || this.props.label || this.props.value, + label: this.children.findType(OptionLabelNode)?.text ?? this.props.value, value: this.props.value, - description: this.props.description, + description: this.children.findType(OptionDescriptionNode)?.text, emoji: this.props.emoji, } } } + +export class OptionLabelNode extends Node<{}> {} +export class OptionDescriptionNode extends Node<{}> {} diff --git a/packages/reacord/library/core/components/option.tsx b/packages/reacord/library/core/components/option.tsx index e27088b..ad060ef 100644 --- a/packages/reacord/library/core/components/option.tsx +++ b/packages/reacord/library/core/components/option.tsx @@ -1,6 +1,11 @@ +import type { ReactNode } from "react" import React from "react" import { ReacordElement } from "../../internal/element" -import { OptionNode } from "./option-node" +import { + OptionDescriptionNode, + OptionLabelNode, + OptionNode, +} from "./option-node" /** * @category Select @@ -9,11 +14,11 @@ export type OptionProps = { /** The internal value of this option */ value: string /** The text shown to the user. This takes priority over `children` */ - label?: string + label?: ReactNode /** The text shown to the user */ - children?: string + children?: ReactNode /** Description for the option, shown to the user */ - description?: string + description?: ReactNode /** * Renders an emoji to the left of the text. @@ -31,8 +36,27 @@ export type OptionProps = { /** * @category Select */ -export function Option(props: OptionProps) { +export function Option({ + label, + children, + description, + ...props +}: OptionProps) { return ( - new OptionNode(props)} /> + new OptionNode(props)}> + {(label !== undefined || children !== undefined) && ( + new OptionLabelNode({})}> + {label || children} + + )} + {description !== undefined && ( + new OptionDescriptionNode({})} + > + {description} + + )} + ) } diff --git a/packages/reacord/library/internal/container.ts b/packages/reacord/library/internal/container.ts index 4dc54c8..8941fcd 100644 --- a/packages/reacord/library/internal/container.ts +++ b/packages/reacord/library/internal/container.ts @@ -21,6 +21,16 @@ export class Container { this.items = [] } + find(predicate: (item: T) => boolean): T | undefined { + return this.items.find(predicate) + } + + findType(type: new (...args: any[]) => U): U | undefined { + for (const item of this.items) { + if (item instanceof type) return item + } + } + [Symbol.iterator]() { return this.items[Symbol.iterator]() } diff --git a/packages/reacord/library/internal/node.ts b/packages/reacord/library/internal/node.ts index 6328865..8efe584 100644 --- a/packages/reacord/library/internal/node.ts +++ b/packages/reacord/library/internal/node.ts @@ -13,4 +13,8 @@ export abstract class Node { handleComponentInteraction(interaction: ComponentInteraction): boolean { return false } + + get text(): string { + return [...this.children].map((child) => child.text).join("") + } } diff --git a/packages/reacord/library/internal/text-node.ts b/packages/reacord/library/internal/text-node.ts index 973b253..ead02ad 100644 --- a/packages/reacord/library/internal/text-node.ts +++ b/packages/reacord/library/internal/text-node.ts @@ -5,4 +5,8 @@ export class TextNode extends Node { override modifyMessageOptions(options: MessageOptions) { options.content = options.content + this.props } + + override get text() { + return this.props + } } diff --git a/packages/reacord/scripts/discordjs-manual-test.tsx b/packages/reacord/scripts/discordjs-manual-test.tsx index f792e6b..ccb4bf4 100644 --- a/packages/reacord/scripts/discordjs-manual-test.tsx +++ b/packages/reacord/scripts/discordjs-manual-test.tsx @@ -81,9 +81,9 @@ await createTest("select", (channel) => { value={value} onChangeValue={setValue} > -