Compare commits

...
4 changed files with 169 additions and 20 deletions
@@ -1 +1,2 @@
export * from './media-picker-folder-path.element.js';
export * from './media-picker-table-column-name.element.js';
@@ -0,0 +1,80 @@
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`<uui-button look="default" compact label=${this.value.name} @click=${this.#onClick}
>${this.value.name}</uui-button
>`
: html`<span class="name">${this.value.name}</span>`}
${this.value.ancestorPath
? html`<span class="ancestor-path">${this.value.ancestorPath}</span>`
: nothing}
`;
}
static override styles = [
UmbTextStyles,
css`
:host {
display: block;
}
uui-button {
--uui-button-padding-left-factor: 0;
text-align: left;
}
.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;
}
}
@@ -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`<umb-media-picker-table-column-name></umb-media-picker-table-column-name>`);
});
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);
});
}
});
@@ -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,
@@ -397,6 +398,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;
@@ -597,6 +603,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,
@@ -604,19 +615,7 @@ export class UmbMediaPickerModalElement extends UmbPickerModalBaseElement<
data: [
{
columnAlias: 'name',
value:
canNavigate && !this._isSelectionMode
? html`<uui-button
look="default"
compact
label=${item.name}
@click=${(e: Event) => {
e.stopPropagation();
this.#onOpen(item);
}}
>${item.name}</uui-button
>`
: html`<span class="table-name">${item.name}</span>`,
value: nameColumnValue,
},
{
columnAlias: 'createDate',
@@ -644,6 +643,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'),
@@ -743,13 +743,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;