diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts
index ccae01905f8c..364cf37ac3a7 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts
@@ -1 +1,2 @@
export * from './media-picker-folder-path.element.js';
+export * from './media-picker-table-column-name.element.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-table-column-name.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-table-column-name.element.ts
new file mode 100644
index 000000000000..46d9135f8bc4
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-table-column-name.element.ts
@@ -0,0 +1,81 @@
+import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
+import type { UmbTableColumn, UmbTableColumnLayoutElement, UmbTableItem } from '@umbraco-cms/backoffice/components';
+
+export interface UmbMediaPickerTableColumnNameValue {
+ name: string;
+ ancestorPath?: string;
+ /** When set, the name renders as a button that invokes this on click (e.g. to open a folder). */
+ navigate?: () => void;
+}
+
+@customElement('umb-media-picker-table-column-name')
+export class UmbMediaPickerTableColumnNameElement extends UmbLitElement implements UmbTableColumnLayoutElement {
+ column!: UmbTableColumn;
+ item!: UmbTableItem;
+
+ @property({ attribute: false })
+ value!: UmbMediaPickerTableColumnNameValue;
+
+ #onClick(event: Event) {
+ event.stopPropagation();
+ this.value?.navigate?.();
+ }
+
+ override render() {
+ if (!this.value) return nothing;
+ return html`
+ ${this.value.navigate
+ ? html`${this.value.name}`
+ : html`${this.value.name}`}
+ ${this.value.ancestorPath
+ ? html`${this.value.ancestorPath}`
+ : nothing}
+ `;
+ }
+
+ static override styles = [
+ UmbTextStyles,
+ css`
+ :host {
+ display: block;
+ }
+
+ uui-button {
+ --uui-button-padding-left-factor: 0;
+ text-align: left;
+ margin-bottom: -9px;
+ }
+
+ .name {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .ancestor-path {
+ display: block;
+ font-size: 0.8em;
+ opacity: 0.6;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ /* Truncate from the start so the deepest (most disambiguating) ancestor stays visible. */
+ direction: rtl;
+ text-align: left;
+ }
+ `,
+ ];
+}
+
+export default UmbMediaPickerTableColumnNameElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-media-picker-table-column-name': UmbMediaPickerTableColumnNameElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-table-column-name.test.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-table-column-name.test.ts
new file mode 100644
index 000000000000..8084fb896566
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-table-column-name.test.ts
@@ -0,0 +1,75 @@
+import { UmbMediaPickerTableColumnNameElement } from './media-picker-table-column-name.element.js';
+import { expect, fixture, html } from '@open-wc/testing';
+import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
+
+describe('UmbMediaPickerTableColumnNameElement', () => {
+ let element: UmbMediaPickerTableColumnNameElement;
+
+ beforeEach(async () => {
+ element = await fixture(html``);
+ });
+
+ it('is defined with its own instance', () => {
+ expect(element).to.be.instanceOf(UmbMediaPickerTableColumnNameElement);
+ });
+
+ it('renders the name', async () => {
+ element.value = { name: 'My image' };
+ await element.updateComplete;
+ expect(element.shadowRoot?.textContent).to.contain('My image');
+ });
+
+ it('renders the ancestor path when provided', async () => {
+ element.value = { name: 'My image', ancestorPath: 'Media / Holiday' };
+ await element.updateComplete;
+ const path = element.shadowRoot?.querySelector('.ancestor-path');
+ expect(path).to.exist;
+ expect(path?.textContent).to.contain('Media / Holiday');
+ });
+
+ it('does not render an ancestor path when not provided', async () => {
+ element.value = { name: 'My image' };
+ await element.updateComplete;
+ expect(element.shadowRoot?.querySelector('.ancestor-path')).to.not.exist;
+ });
+
+ it('renders the name as a plain span when not navigable', async () => {
+ element.value = { name: 'My image' };
+ await element.updateComplete;
+ expect(element.shadowRoot?.querySelector('uui-button')).to.not.exist;
+ expect(element.shadowRoot?.querySelector('span.name')).to.exist;
+ });
+
+ it('renders the name as a button and invokes navigate on click', async () => {
+ let navigated = false;
+ element.value = { name: 'A folder', navigate: () => (navigated = true) };
+ await element.updateComplete;
+
+ const button = element.shadowRoot?.querySelector('uui-button');
+ expect(button).to.exist;
+
+ button!.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
+ expect(navigated).to.be.true;
+ });
+
+ it('stops the click from bubbling so it does not trigger row selection', async () => {
+ element.value = { name: 'A folder', navigate: () => {} };
+ await element.updateComplete;
+
+ const button = element.shadowRoot?.querySelector('uui-button');
+
+ let bubbledToHost = false;
+ element.addEventListener('click', () => (bubbledToHost = true));
+ button!.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
+
+ expect(bubbledToHost).to.be.false;
+ });
+
+ if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
+ it('passes the a11y audit', async () => {
+ element.value = { name: 'My image', ancestorPath: 'Media / Holiday' };
+ await element.updateComplete;
+ await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
+ });
+ }
+});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts
index be4d9146506e..d6fe9d90794a 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts
@@ -7,6 +7,7 @@ import type { UmbMediaTreeItemModel, UmbMediaSearchItemModel, UmbMediaItemModel
import { UmbMediaPickerContext } from './media-picker.context.js';
import type { UmbMediaPathModel } from './types.js';
import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js';
+import type { UmbMediaPickerTableColumnNameValue } from './components/media-picker-table-column-name.element.js';
import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js';
import {
css,
@@ -469,6 +470,11 @@ export class UmbMediaPickerModalElement extends UmbPickerModalBaseElement<
return this.#folderTypeUniques.has(item.mediaType.unique) || item.hasChildren;
}
+ #getAncestorPath(item: UmbMediaTreeItemModel | UmbMediaSearchItemModel): string {
+ const ancestors = 'ancestors' in item ? item.ancestors : undefined;
+ return ancestors?.length ? ancestors.map((a) => a.name || '(Untitled)').join(' / ') : '';
+ }
+
// TODO: move to search manager in context
#onSearchFromChange(e: CustomEvent) {
const checked = (e.target as HTMLInputElement).checked;
@@ -669,6 +675,11 @@ export class UmbMediaPickerModalElement extends UmbPickerModalBaseElement<
// When not in selection mode, navigable items open on click (matching card behaviour).
// Mark them as non-selectable so the row doesn't intercept the click for selection.
const selectableInTable = this._isSelectionMode ? selectable : !canNavigate && selectable;
+ const nameColumnValue: UmbMediaPickerTableColumnNameValue = {
+ name: item.name,
+ ancestorPath: this.#getAncestorPath(item) || undefined,
+ navigate: canNavigate && !this._isSelectionMode ? () => this.#onOpen(item) : undefined,
+ };
return {
id: item.unique,
icon: item.mediaType.icon,
@@ -676,19 +687,7 @@ export class UmbMediaPickerModalElement extends UmbPickerModalBaseElement<
data: [
{
columnAlias: 'name',
- value:
- canNavigate && !this._isSelectionMode
- ? html` {
- e.stopPropagation();
- this.#onOpen(item);
- }}
- >${item.name}`
- : html`${item.name}`,
+ value: nameColumnValue,
},
{
columnAlias: 'createDate',
@@ -716,6 +715,7 @@ export class UmbMediaPickerModalElement extends UmbPickerModalBaseElement<
{
name: this.localize.term('general_name'),
alias: 'name',
+ elementName: 'umb-media-picker-table-column-name',
},
{
name: this.localize.term('content_createDate'),
@@ -815,13 +815,6 @@ export class UmbMediaPickerModalElement extends UmbPickerModalBaseElement<
margin-top: var(--uui-size-layout-1);
}
- .table-name {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
#view-dropdown {
padding: var(--uui-size-space-3);
--uui-button-content-align: left;