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
* @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

View File

@@ -34,13 +34,23 @@ export type ButtonClickEvent = ComponentEvent
*/
export function Button(props: ButtonProps) {
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> {
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<ButtonProps> {
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<ButtonProps> {
return false
}
}
class ButtonLabelNode extends Node<{}> {}

View File

@@ -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 (
<ReacordElement
props={props}
createNode={() => new EmbedAuthorNode(props)}
/>
<ReacordElement props={props} createNode={() => new EmbedAuthorNode(props)}>
<ReacordElement props={{}} createNode={() => new AuthorTextNode({})}>
{props.name ?? props.children}
</ReacordElement>
</ReacordElement>
)
}
class EmbedAuthorNode extends EmbedChildNode<EmbedAuthorProps> {
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<{}> {}

View File

@@ -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 (
<ReacordElement
props={props}
createNode={() => new EmbedFieldNode(props)}
/>
<ReacordElement props={props} createNode={() => new EmbedFieldNode(props)}>
<ReacordElement props={{}} createNode={() => new FieldNameNode({})}>
{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 {
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<{}> {}

View File

@@ -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 (
<ReacordElement
props={props}
createNode={() => new EmbedFooterNode(props)}
/>
<ReacordElement props={props} createNode={() => new EmbedFooterNode(props)}>
<ReacordElement props={{}} createNode={() => new FooterTextNode({})}>
{text ?? children}
</ReacordElement>
</ReacordElement>
)
}
class EmbedFooterNode extends EmbedChildNode<EmbedFooterProps> {
class EmbedFooterNode extends EmbedChildNode<
Omit<EmbedFooterProps, "text" | "children">
> {
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<EmbedFooterProps> {
: undefined
}
}
class FooterTextNode extends Node<{}> {}

View File

@@ -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 (
<ReacordElement
props={props}
createNode={() => new EmbedTitleNode(props)}
/>
<ReacordElement props={props} createNode={() => new EmbedTitleNode(props)}>
<ReacordElement props={{}} createNode={() => new TitleTextNode({})}>
{children}
</ReacordElement>
</ReacordElement>
)
}
class EmbedTitleNode extends EmbedChildNode<EmbedTitleProps> {
class EmbedTitleNode extends EmbedChildNode<Omit<EmbedTitleProps, "children">> {
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<{}> {}

View File

@@ -18,18 +18,26 @@ export type LinkProps = ButtonSharedProps & {
/**
* @category Link
*/
export function Link(props: LinkProps) {
return <ReacordElement props={props} createNode={() => new LinkNode(props)} />
export function Link({ label, children, ...props }: LinkProps) {
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 {
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<{}> {}

View File

@@ -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<OptionProps> {
export class OptionNode extends Node<
Omit<OptionProps, "children" | "label" | "description">
> {
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<{}> {}

View File

@@ -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 (
<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 = []
}
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]() {
return this.items[Symbol.iterator]()
}

View File

@@ -13,4 +13,8 @@ export abstract class Node<Props> {
handleComponentInteraction(interaction: ComponentInteraction): boolean {
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) {
options.content = options.content + this.props
}
override get text() {
return this.props
}
}

View File

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