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
+
+
+
+
+
Showing node: {{ node.source }}
+
... in folder: {{ folder.source }}
+
... with view: {{ view.id }}
+
+
+```
+
+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.
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')
}
}