allow JSX for text in more places
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<{}> {}
|
||||
|
||||
@@ -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<{}> {}
|
||||
|
||||
@@ -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<{}> {}
|
||||
|
||||
@@ -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<{}> {}
|
||||
|
||||
@@ -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<{}> {}
|
||||
|
||||
@@ -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<{}> {}
|
||||
|
||||
@@ -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<{}> {}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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]()
|
||||
}
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
89
packages/reacord/test/text-children.test.tsx
Normal file
89
packages/reacord/test/text-children.test.tsx
Normal 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" },
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user