allow JSX for text in more places

This commit is contained in:
itsMapleLeaf
2022-07-22 21:28:14 -05:00
committed by Darius
parent 9afe6fe0fa
commit bc91080eca
14 changed files with 236 additions and 52 deletions

View File

@@ -1,10 +1,12 @@
import type { ReactNode } from "react"
/** /**
* Common props between button-like components * Common props between button-like components
* @category Button * @category Button
*/ */
export type ButtonSharedProps = { export type ButtonSharedProps = {
/** The text on the button. Rich formatting (markdown) is not supported here. */ /** 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. */ /** When true, the button will be slightly faded, and cannot be clicked. */
disabled?: boolean disabled?: boolean

View File

@@ -34,13 +34,23 @@ export type ButtonClickEvent = ComponentEvent
*/ */
export function Button(props: ButtonProps) { export function Button(props: ButtonProps) {
return ( return (
<ReacordElement props={props} createNode={() => new ButtonNode(props)} /> <ReacordElement props={props} createNode={() => new ButtonNode(props)}>
<ReacordElement props={{}} createNode={() => new ButtonLabelNode({})}>
{props.label}
</ReacordElement>
</ReacordElement>
) )
} }
class ButtonNode extends Node<ButtonProps> { class ButtonNode extends Node<ButtonProps> {
private customId = nanoid() 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 { override modifyMessageOptions(options: MessageOptions): void {
getNextActionRow(options).push({ getNextActionRow(options).push({
type: "button", type: "button",
@@ -48,7 +58,7 @@ class ButtonNode extends Node<ButtonProps> {
style: this.props.style ?? "secondary", style: this.props.style ?? "secondary",
disabled: this.props.disabled, disabled: this.props.disabled,
emoji: this.props.emoji, emoji: this.props.emoji,
label: this.props.label, label: this.children.findType(ButtonLabelNode)?.text,
}) })
} }
@@ -63,3 +73,5 @@ class ButtonNode extends Node<ButtonProps> {
return false return false
} }
} }
class ButtonLabelNode extends Node<{}> {}

View File

@@ -1,5 +1,7 @@
import type { ReactNode } from "react"
import React from "react" import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import { Node } from "../../internal/node.js"
import { EmbedChildNode } from "./embed-child.js" import { EmbedChildNode } from "./embed-child.js"
import type { EmbedOptions } from "./embed-options" import type { EmbedOptions } from "./embed-options"
@@ -7,8 +9,8 @@ import type { EmbedOptions } from "./embed-options"
* @category Embed * @category Embed
*/ */
export type EmbedAuthorProps = { export type EmbedAuthorProps = {
name?: string name?: ReactNode
children?: string children?: ReactNode
url?: string url?: string
iconUrl?: string iconUrl?: string
} }
@@ -18,19 +20,22 @@ export type EmbedAuthorProps = {
*/ */
export function EmbedAuthor(props: EmbedAuthorProps) { export function EmbedAuthor(props: EmbedAuthorProps) {
return ( return (
<ReacordElement <ReacordElement props={props} createNode={() => new EmbedAuthorNode(props)}>
props={props} <ReacordElement props={{}} createNode={() => new AuthorTextNode({})}>
createNode={() => new EmbedAuthorNode(props)} {props.name ?? props.children}
/> </ReacordElement>
</ReacordElement>
) )
} }
class EmbedAuthorNode extends EmbedChildNode<EmbedAuthorProps> { class EmbedAuthorNode extends EmbedChildNode<EmbedAuthorProps> {
override modifyEmbedOptions(options: EmbedOptions): void { override modifyEmbedOptions(options: EmbedOptions): void {
options.author = { options.author = {
name: this.props.name ?? this.props.children ?? "", name: this.children.findType(AuthorTextNode)?.text ?? "",
url: this.props.url, url: this.props.url,
icon_url: this.props.iconUrl, icon_url: this.props.iconUrl,
} }
} }
} }
class AuthorTextNode extends Node<{}> {}

View File

@@ -1,5 +1,7 @@
import type { ReactNode } from "react"
import React from "react" import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import { Node } from "../../internal/node.js"
import { EmbedChildNode } from "./embed-child.js" import { EmbedChildNode } from "./embed-child.js"
import type { EmbedOptions } from "./embed-options" import type { EmbedOptions } from "./embed-options"
@@ -7,10 +9,10 @@ import type { EmbedOptions } from "./embed-options"
* @category Embed * @category Embed
*/ */
export type EmbedFieldProps = { export type EmbedFieldProps = {
name: string name: ReactNode
value?: string value?: ReactNode
inline?: boolean inline?: boolean
children?: string children?: ReactNode
} }
/** /**
@@ -18,10 +20,14 @@ export type EmbedFieldProps = {
*/ */
export function EmbedField(props: EmbedFieldProps) { export function EmbedField(props: EmbedFieldProps) {
return ( return (
<ReacordElement <ReacordElement props={props} createNode={() => new EmbedFieldNode(props)}>
props={props} <ReacordElement props={{}} createNode={() => new FieldNameNode({})}>
createNode={() => new EmbedFieldNode(props)} {props.name}
/> </ReacordElement>
<ReacordElement props={{}} createNode={() => new FieldValueNode({})}>
{props.value || props.children}
</ReacordElement>
</ReacordElement>
) )
} }
@@ -29,9 +35,12 @@ class EmbedFieldNode extends EmbedChildNode<EmbedFieldProps> {
override modifyEmbedOptions(options: EmbedOptions): void { override modifyEmbedOptions(options: EmbedOptions): void {
options.fields ??= [] options.fields ??= []
options.fields.push({ options.fields.push({
name: this.props.name, name: this.children.findType(FieldNameNode)?.text ?? "",
value: this.props.value ?? this.props.children ?? "", value: this.children.findType(FieldValueNode)?.text ?? "",
inline: this.props.inline, inline: this.props.inline,
}) })
} }
} }
class FieldNameNode extends Node<{}> {}
class FieldValueNode extends Node<{}> {}

View File

@@ -1,5 +1,7 @@
import type { ReactNode } from "react"
import React from "react" import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import { Node } from "../../internal/node.js"
import { EmbedChildNode } from "./embed-child.js" import { EmbedChildNode } from "./embed-child.js"
import type { EmbedOptions } from "./embed-options" import type { EmbedOptions } from "./embed-options"
@@ -7,8 +9,8 @@ import type { EmbedOptions } from "./embed-options"
* @category Embed * @category Embed
*/ */
export type EmbedFooterProps = { export type EmbedFooterProps = {
text?: string text?: ReactNode
children?: string children?: ReactNode
iconUrl?: string iconUrl?: string
timestamp?: string | number | Date timestamp?: string | number | Date
} }
@@ -16,19 +18,22 @@ export type EmbedFooterProps = {
/** /**
* @category Embed * @category Embed
*/ */
export function EmbedFooter(props: EmbedFooterProps) { export function EmbedFooter({ text, children, ...props }: EmbedFooterProps) {
return ( return (
<ReacordElement <ReacordElement props={props} createNode={() => new EmbedFooterNode(props)}>
props={props} <ReacordElement props={{}} createNode={() => new FooterTextNode({})}>
createNode={() => new EmbedFooterNode(props)} {text ?? children}
/> </ReacordElement>
</ReacordElement>
) )
} }
class EmbedFooterNode extends EmbedChildNode<EmbedFooterProps> { class EmbedFooterNode extends EmbedChildNode<
Omit<EmbedFooterProps, "text" | "children">
> {
override modifyEmbedOptions(options: EmbedOptions): void { override modifyEmbedOptions(options: EmbedOptions): void {
options.footer = { options.footer = {
text: this.props.text ?? this.props.children ?? "", text: this.children.findType(FooterTextNode)?.text ?? "",
icon_url: this.props.iconUrl, icon_url: this.props.iconUrl,
} }
options.timestamp = this.props.timestamp options.timestamp = this.props.timestamp
@@ -36,3 +41,5 @@ class EmbedFooterNode extends EmbedChildNode<EmbedFooterProps> {
: undefined : undefined
} }
} }
class FooterTextNode extends Node<{}> {}

View File

@@ -1,5 +1,7 @@
import type { ReactNode } from "react"
import React from "react" import React from "react"
import { ReacordElement } from "../../internal/element.js" import { ReacordElement } from "../../internal/element.js"
import { Node } from "../../internal/node.js"
import { EmbedChildNode } from "./embed-child.js" import { EmbedChildNode } from "./embed-child.js"
import type { EmbedOptions } from "./embed-options" import type { EmbedOptions } from "./embed-options"
@@ -7,25 +9,28 @@ import type { EmbedOptions } from "./embed-options"
* @category Embed * @category Embed
*/ */
export type EmbedTitleProps = { export type EmbedTitleProps = {
children: string children: ReactNode
url?: string url?: string
} }
/** /**
* @category Embed * @category Embed
*/ */
export function EmbedTitle(props: EmbedTitleProps) { export function EmbedTitle({ children, ...props }: EmbedTitleProps) {
return ( return (
<ReacordElement <ReacordElement props={props} createNode={() => new EmbedTitleNode(props)}>
props={props} <ReacordElement props={{}} createNode={() => new TitleTextNode({})}>
createNode={() => new EmbedTitleNode(props)} {children}
/> </ReacordElement>
</ReacordElement>
) )
} }
class EmbedTitleNode extends EmbedChildNode<EmbedTitleProps> { class EmbedTitleNode extends EmbedChildNode<Omit<EmbedTitleProps, "children">> {
override modifyEmbedOptions(options: EmbedOptions): void { override modifyEmbedOptions(options: EmbedOptions): void {
options.title = this.props.children options.title = this.children.findType(TitleTextNode)?.text ?? ""
options.url = this.props.url options.url = this.props.url
} }
} }
class TitleTextNode extends Node<{}> {}

View File

@@ -18,18 +18,26 @@ export type LinkProps = ButtonSharedProps & {
/** /**
* @category Link * @category Link
*/ */
export function Link(props: LinkProps) { export function Link({ label, children, ...props }: LinkProps) {
return <ReacordElement props={props} createNode={() => new LinkNode(props)} /> return (
<ReacordElement props={props} createNode={() => new LinkNode(props)}>
<ReacordElement props={{}} createNode={() => new LinkTextNode({})}>
{label || children}
</ReacordElement>
</ReacordElement>
)
} }
class LinkNode extends Node<LinkProps> { class LinkNode extends Node<Omit<LinkProps, "label" | "children">> {
override modifyMessageOptions(options: MessageOptions): void { override modifyMessageOptions(options: MessageOptions): void {
getNextActionRow(options).push({ getNextActionRow(options).push({
type: "link", type: "link",
disabled: this.props.disabled, disabled: this.props.disabled,
emoji: this.props.emoji, emoji: this.props.emoji,
label: this.props.label || this.props.children, label: this.children.findType(LinkTextNode)?.text,
url: this.props.url, url: this.props.url,
}) })
} }
} }
class LinkTextNode extends Node<{}> {}

View File

@@ -2,13 +2,18 @@ import type { MessageSelectOptionOptions } from "../../internal/message"
import { Node } from "../../internal/node" import { Node } from "../../internal/node"
import type { OptionProps } from "./option" import type { OptionProps } from "./option"
export class OptionNode extends Node<OptionProps> { export class OptionNode extends Node<
Omit<OptionProps, "children" | "label" | "description">
> {
get options(): MessageSelectOptionOptions { get options(): MessageSelectOptionOptions {
return { return {
label: this.props.children || this.props.label || this.props.value, label: this.children.findType(OptionLabelNode)?.text ?? this.props.value,
value: this.props.value, value: this.props.value,
description: this.props.description, description: this.children.findType(OptionDescriptionNode)?.text,
emoji: this.props.emoji, emoji: this.props.emoji,
} }
} }
} }
export class OptionLabelNode extends Node<{}> {}
export class OptionDescriptionNode extends Node<{}> {}

View File

@@ -1,6 +1,11 @@
import type { ReactNode } from "react"
import React from "react" import React from "react"
import { ReacordElement } from "../../internal/element" import { ReacordElement } from "../../internal/element"
import { OptionNode } from "./option-node" import {
OptionDescriptionNode,
OptionLabelNode,
OptionNode,
} from "./option-node"
/** /**
* @category Select * @category Select
@@ -9,11 +14,11 @@ export type OptionProps = {
/** The internal value of this option */ /** The internal value of this option */
value: string value: string
/** The text shown to the user. This takes priority over `children` */ /** The text shown to the user. This takes priority over `children` */
label?: string label?: ReactNode
/** The text shown to the user */ /** The text shown to the user */
children?: string children?: ReactNode
/** Description for the option, shown to the user */ /** Description for the option, shown to the user */
description?: string description?: ReactNode
/** /**
* Renders an emoji to the left of the text. * Renders an emoji to the left of the text.
@@ -31,8 +36,27 @@ export type OptionProps = {
/** /**
* @category Select * @category Select
*/ */
export function Option(props: OptionProps) { export function Option({
label,
children,
description,
...props
}: OptionProps) {
return ( return (
<ReacordElement props={props} createNode={() => new OptionNode(props)} /> <ReacordElement props={props} createNode={() => new OptionNode(props)}>
{(label !== undefined || children !== undefined) && (
<ReacordElement props={{}} createNode={() => new OptionLabelNode({})}>
{label || children}
</ReacordElement>
)}
{description !== undefined && (
<ReacordElement
props={{}}
createNode={() => new OptionDescriptionNode({})}
>
{description}
</ReacordElement>
)}
</ReacordElement>
) )
} }

View File

@@ -21,6 +21,16 @@ export class Container<T> {
this.items = [] this.items = []
} }
find(predicate: (item: T) => boolean): T | undefined {
return this.items.find(predicate)
}
findType<U extends T>(type: new (...args: any[]) => U): U | undefined {
for (const item of this.items) {
if (item instanceof type) return item
}
}
[Symbol.iterator]() { [Symbol.iterator]() {
return this.items[Symbol.iterator]() return this.items[Symbol.iterator]()
} }

View File

@@ -13,4 +13,8 @@ export abstract class Node<Props> {
handleComponentInteraction(interaction: ComponentInteraction): boolean { handleComponentInteraction(interaction: ComponentInteraction): boolean {
return false return false
} }
get text(): string {
return [...this.children].map((child) => child.text).join("")
}
} }

View File

@@ -5,4 +5,8 @@ export class TextNode extends Node<string> {
override modifyMessageOptions(options: MessageOptions) { override modifyMessageOptions(options: MessageOptions) {
options.content = options.content + this.props options.content = options.content + this.props
} }
override get text() {
return this.props
}
} }

View File

@@ -81,9 +81,9 @@ await createTest("select", (channel) => {
value={value} value={value}
onChangeValue={setValue} onChangeValue={setValue}
> >
<Option value="🍎" /> <Option value="🍎" emoji="🍎" label="apple" description="it red" />
<Option value="🍌" /> <Option value="🍌" emoji="🍌" label="banana" description="bnanbna" />
<Option value="🍒" /> <Option value="🍒" emoji="🍒" label="cherry" description="heh" />
</Select> </Select>
<Button <Button
label="confirm" label="confirm"

View File

@@ -0,0 +1,89 @@
import * as React from "react"
import { test } from "vitest"
import {
Button,
Embed,
EmbedAuthor,
EmbedField,
EmbedFooter,
EmbedTitle,
Link,
Option,
Select,
} from "../library/main"
import { ReacordTester } from "./test-adapter"
test("text children in other components", async () => {
const tester = new ReacordTester()
const SomeText = () => <>some text</>
await tester.assertRender(
<>
<Embed>
<EmbedTitle>
<SomeText />
</EmbedTitle>
<EmbedAuthor>
<SomeText />
</EmbedAuthor>
<EmbedField name={<SomeText />}>
<SomeText /> <Button label="ignore this" onClick={() => {}} />
nailed it
</EmbedField>
<EmbedFooter>
<SomeText />
</EmbedFooter>
</Embed>
<Button label={<SomeText />} onClick={() => {}} />
<Link url="https://discord.com" label={<SomeText />} />
<Select>
<Option value="1">
<SomeText />
</Option>
<Option value="2" label={<SomeText />} description={<SomeText />} />
</Select>
</>,
[
{
content: "",
embeds: [
{
title: "some text",
author: {
name: "some text",
},
fields: [{ name: "some text", value: "some text nailed it" }],
footer: {
text: "some text",
},
},
],
actionRows: [
[
{
type: "button",
label: "some text",
style: "secondary",
},
{
type: "link",
url: "https://discord.com",
label: "some text",
},
],
[
{
type: "select",
values: [],
options: [
{ value: "1", label: "some text" },
{ value: "2", label: "some text", description: "some text" },
],
},
],
],
},
],
)
})