From b0e855a9df003a94257b9c488ed6489f75b932b9 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 14 Jan 2026 16:29:33 +0100 Subject: [PATCH 1/2] feat(SidebarTab): allow to define web component on first usage Signed-off-by: Ferdinand Thiessen --- __tests__/sidebar/sidebarTab.spec.ts | 15 +++++++--- lib/sidebar/SidebarTab.ts | 42 +++++++++++++++++++++------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/__tests__/sidebar/sidebarTab.spec.ts b/__tests__/sidebar/sidebarTab.spec.ts index 47ed3cbfa..4f84de36d 100644 --- a/__tests__/sidebar/sidebarTab.spec.ts +++ b/__tests__/sidebar/sidebarTab.spec.ts @@ -77,7 +77,7 @@ describe('Sidebar tabs', () => { it('fails with invalid tagName name', () => { expect(() => registerSidebarTab({ ...getExampleTab(), tagName: 'MyAppSidebarTab' })) - .toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tabs tagName name is invalid]') + .toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tab "tagName" is invalid]') }) it('fails with missing name', () => { @@ -119,11 +119,18 @@ describe('Sidebar tabs', () => { }).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tabs need to have a numeric order set]') }) - it('fails with missing "enabled" method', () => { + it('fails with invalid "enabled" method', () => { expect(() => { // @ts-expect-error mocking for testing - registerSidebarTab({ ...getExampleTab(), enabled: undefined }) - }).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tabs need to have an "enabled" method]') + registerSidebarTab({ ...getExampleTab(), enabled: 'true' }) + }).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tab "enabled" is not a function]') + }) + + it('fails with invalid "onInit" method', () => { + expect(() => { + // @ts-expect-error mocking for testing + registerSidebarTab({ ...getExampleTab(), onInit: 'not a method' }) + }).toThrowErrorMatchingInlineSnapshot('[Error: Sidebar tab "onInit" is not a function]') }) }) }) diff --git a/lib/sidebar/SidebarTab.ts b/lib/sidebar/SidebarTab.ts index b2d54dc79..d1303b19e 100644 --- a/lib/sidebar/SidebarTab.ts +++ b/lib/sidebar/SidebarTab.ts @@ -32,15 +32,19 @@ export interface ISidebarContext { * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_components * @see https://vuejs.org/guide/extras/web-components#building-custom-elements-with-vue */ -export interface SidebarComponent extends HTMLElement, ISidebarContext { +export interface SidebarTabComponent extends ISidebarContext { /** - * This method is called by the files app if the sidebar tab state changes. - * - * @param active - The new active state + * The active state of the sidebar tab. + * It will be set to true if this component is the currently active tab. */ - setActive(active: boolean): Promise + active: boolean } +/** + * The instance type of a sidebar tab web component. + */ +export type SidebarTabComponentInstance = SidebarTabComponent & HTMLElement + /** * Implementation of a custom sidebar tab within the files app. */ @@ -69,7 +73,10 @@ export interface ISidebarTab { /** * The tag name of the web component. - * The web component must already be registered under that tag name with `CustomElementRegistry.define()`. + * + * The web component must be defined using this name with `CustomElementRegistry.define()`, + * either on initialization or within the `onInit` callback (preferred). + * When rendering the sidebar tab, the files app will wait for the component to be defined in the registry (`customElements.whenDefined()`). * * To avoid name clashes the name has to start with your appid (e.g. `your_app`). * So in addition with the web component naming rules a good name would be `your_app-files-sidebar-tab`. @@ -79,9 +86,20 @@ export interface ISidebarTab { /** * Callback to check if the sidebar tab should be shown for the selected node. * + * If not provided, the tab will always be shown. + * * @param context - The current context of the files app */ - enabled: (context: ISidebarContext) => boolean + enabled?: (context: ISidebarContext) => boolean + + /** + * Called when the sidebar tab is active and rendered the first time in the sidebar. + * This should be used to register the web componen (`CustomElementRegistry.define()`). + * + * The sidebar itself will anyways wait for the component to be defined in the registry (`customElements.whenDefined()`). + * But also will wait for the promise returned by this method to resolve before rendering the tab. + */ + onInit?: () => Promise } /** @@ -132,7 +150,7 @@ function validateSidebarTab(tab: ISidebarTab): void { } if (!tab.tagName.match(/^[a-z][a-z0-9-_]+$/)) { - throw new Error('Sidebar tabs tagName name is invalid') + throw new Error('Sidebar tab "tagName" is invalid') } if (!tab.displayName || typeof tab.displayName !== 'string') { @@ -147,7 +165,11 @@ function validateSidebarTab(tab: ISidebarTab): void { throw new Error('Sidebar tabs need to have a numeric order set') } - if (typeof tab.enabled !== 'function') { - throw new Error('Sidebar tabs need to have an "enabled" method') + if (tab.enabled && typeof tab.enabled !== 'function') { + throw new Error('Sidebar tab "enabled" is not a function') + } + + if (tab.onInit && typeof tab.onInit !== 'function') { + throw new Error('Sidebar tab "onInit" is not a function') } } From 6bf0138025b3bf61e876325b21b4901f8be9e6d1 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 15 Jan 2026 12:40:53 +0100 Subject: [PATCH 2/2] docs: add sidebar tab example to README Co-authored-by: Ferdinand Thiessen Co-authored-by: Grigorii K. Shartsev Signed-off-by: Ferdinand Thiessen --- README.md | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/README.md b/README.md index 522a5c619..d268c5a7e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,123 @@ const myEntry: Entry = { addNewFileMenuEntry(myEntry) ``` +#### Register a sidebar tab + +It is possible to provide your own sidebar tabs for the files app. +For this you need to create a [custom web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components), +which can either be done without any framework by using vanilla JavaScript but is also [possible with Vue](https://vuejs.org/guide/extras/web-components#building-custom-elements-with-vue). + +This example will make use of the Vue framework for building a sidebar tab as this is the official UI framework for Nextcloud apps. + +The sidebar tab consists of two parts: +1. The web component which will be rendered within the sidebar. +2. A definition object that provides all information needed by the files app. + +##### SidebarTab definition object + +This object provides the requires information such as: +- The order (to ensure a consistent tabs order) +- The display name for the tab navigation +- An icon, to be used in the tab navigation +- A callback to check if the sidebar tab is enabled for the current node shown in the sidebar. +- The web component tag name + +The registration must happen in an `initScript`. + +```ts +import type { ISidebarTab } from '@nextcloud/files' + +import { getSidebar } from '@nextcloud/files' +import { t } from '@nextcloud/l10n' + +const MyTab: ISidebarTab = { + // Unique ID of the tab + id: 'my_app', + + // The display name in the tab list + displayName: t('my_app', 'Sharing'), + + // Pass an SVG (string) to be used as the tab button icon + iconSvgInline: '...', + + // Lower values mean a more prominent position + order: 50, + + // The tag name of the web component + tagName: 'my_app-files_sidebar_tab', + + // Optional callback to check if the tab should be shown + enabled({ node, folder, view }) { + // you can disable this tab for some cased based on: + // - node: The node the sidebar was opened for + // - folder: The folder currently shown in the files app + // - view: The currently active files view + return true + }, + + // Optional, recommended to large tabs + async onInit() { + // This is called when the tab is about to be activated the first time. + // So this can be used to do some initialization or even to define the web component. + }, +} + +// the you need to register it in the sidebar +getSidebar() + .registerTab(MyTab) +``` + +##### SidebarTab web component + +The web component needs to have those properties: +- node of type `INode` +- folder of type `IFolder` +- view of type `IView` +- active of type `boolean` + +When using Vue you need to first create the Vue component: + +```vue + + + +``` + +Which then can be wrapped in a web component and registered. + +```ts +import { getSidebar } from '@nextcloud/files' +import { defineAsyncComponent, defineCustomElement } from 'vue' + +getSidebar().registerTab({ + // ... + + tagName: `my_app-files_sidebar_tab`, + + onInit() { + const MySidebarTab = defineAsyncComponent(() => import('./views/MySidebarTab.vue')) + // make sure to disable the shadow root to allow theming with Nextcloud provided global styles. + const MySidebarTabWebComponent = defineCustomElement(MySidebarTab, { shadowRoot: false }) + customElements.define('my_app-files_sidebar_tab', MySidebarTabWebComponent) + }, +}) +``` + ### WebDAV The `getClient` exported function returns a webDAV client that's a wrapper around [webdav's webDAV client](https://www.npmjs.com/package/webdav). All its methods are available here.