diff --git a/ui/src/components/channel-icon.ts b/ui/src/components/channel-icon.ts new file mode 100644 index 000000000000..e0b87a0464b2 --- /dev/null +++ b/ui/src/components/channel-icon.ts @@ -0,0 +1,30 @@ +import { html } from "lit"; +import { + pluginArtPath, + pluginFallbackGradient, + pluginMonogram, +} from "../pages/plugins/presentation.ts"; +import "../styles/channels.css"; + +/** Bundled channel art reuses the plugin art set because channel ids match plugin slugs. */ +export function renderChannelIcon( + channelId: string, + label: string, + variant: "tile" | "cover" | "picker", +) { + const artVariant = variant === "picker" ? "tile" : variant; + const art = pluginArtPath(channelId); + const [from, to] = art ? ["", ""] : pluginFallbackGradient(channelId); + const style = `${variant === "picker" ? "--channels-art-size:24px;" : ""}${ + art ? "" : `--channels-art-a:${from};--channels-art-b:${to}` + }`; + return html``; +} diff --git a/ui/src/components/channel-picker.test.ts b/ui/src/components/channel-picker.test.ts new file mode 100644 index 000000000000..55dd805d6e9f --- /dev/null +++ b/ui/src/components/channel-picker.test.ts @@ -0,0 +1,64 @@ +/* @vitest-environment jsdom */ +import { render } from "lit"; +import { describe, expect, it, vi } from "vitest"; +import { renderChannelPicker } from "./channel-picker.ts"; + +describe("renderChannelPicker", () => { + it("renders neutral and channel artwork while preserving a missing current channel", () => { + const container = document.createElement("div"); + render( + renderChannelPicker({ + label: "Channel", + value: "retired-channel", + options: [ + { value: "last", label: "last", kind: "neutral" }, + { value: "telegram", label: "Telegram" }, + ], + onChange: vi.fn(), + }), + container, + ); + + expect(container.querySelector('wa-option[value="last"] [slot="start"]')).toBeNull(); + expect(container.querySelector('wa-option[value="telegram"] img')).not.toBeNull(); + expect(container.querySelector('wa-option[value="retired-channel"]')?.textContent).toContain( + "retired-channel", + ); + expect( + container.querySelector('wa-option[value="retired-channel"] .channels-tile--fallback'), + ).not.toBeNull(); + }); + + it("honors disabled choices and reports enabled changes", () => { + const container = document.createElement("div"); + const onChange = vi.fn(); + render( + renderChannelPicker({ + label: "Channel", + value: "telegram", + options: [ + { value: "telegram", label: "Telegram" }, + { value: "disabled", label: "Disabled", disabled: true }, + ], + onChange, + }), + container, + ); + + const picker = container.querySelector("wa-select"); + expect(container.querySelector('wa-option[value="disabled"]')?.hasAttribute("disabled")).toBe( + true, + ); + if (!picker) { + return; + } + Object.defineProperty(picker, "value", { configurable: true, value: "disabled" }); + picker.dispatchEvent(new Event("change", { bubbles: true })); + Reflect.deleteProperty(picker, "value"); + expect(onChange).not.toHaveBeenCalled(); + Object.defineProperty(picker, "value", { configurable: true, value: "telegram" }); + picker.dispatchEvent(new Event("change", { bubbles: true })); + Reflect.deleteProperty(picker, "value"); + expect(onChange).toHaveBeenCalledWith("telegram"); + }); +}); diff --git a/ui/src/components/channel-picker.ts b/ui/src/components/channel-picker.ts new file mode 100644 index 000000000000..21942b21c9f6 --- /dev/null +++ b/ui/src/components/channel-picker.ts @@ -0,0 +1,17 @@ +import { nothing } from "lit"; +import { renderChannelIcon } from "./channel-icon.ts"; +import { renderPicker, type PickerOption, type PickerParams } from "./select-picker.ts"; + +export type ChannelPickerOption = PickerOption & { + /** Neutral choices such as "last" or "all" are routing policy, not transports. */ + kind?: "channel" | "neutral"; +}; + +export function renderChannelPicker(params: PickerParams) { + return renderPicker({ + ...params, + className: "channel-picker", + renderLeading: (option) => + option.kind === "neutral" ? nothing : renderChannelIcon(option.value, option.label, "picker"), + }); +} diff --git a/ui/src/components/select-picker.ts b/ui/src/components/select-picker.ts new file mode 100644 index 000000000000..ee5e666bd59c --- /dev/null +++ b/ui/src/components/select-picker.ts @@ -0,0 +1,71 @@ +import { html, nothing } from "lit"; +import "./web-awesome-select.ts"; + +export type PickerOption = { + value: string; + label: string; + description?: string; + disabled?: boolean; +}; + +export type PickerParams