diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts index f928869b69..a643b8be78 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts @@ -4,6 +4,8 @@ import { manifests as memberGroupManifests } from './member-groups/manifests.js' import { manifests as memberTypeManifests } from './member-types/manifests.js'; import { manifests as memberManifests } from './members/manifests.js'; +import './members/components/index.js'; + export const manifests = [ ...memberSectionManifests, ...menuSectionManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/members/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/members/components/index.ts new file mode 100644 index 0000000000..90f47707f8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/members/components/index.ts @@ -0,0 +1,3 @@ +import './input-member/input-member.element.js'; + +export * from './input-member/input-member.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/members/components/input-member/input-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/members/components/input-member/input-member.element.ts new file mode 100644 index 0000000000..d1106e1f2e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/members/components/input-member/input-member.element.ts @@ -0,0 +1,171 @@ +import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import type { MemberItemResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; + +@customElement('umb-input-member') +export class UmbInputMemberElement extends FormControlMixin(UmbLitElement) { + /** + * This is a minimum amount of selected items in this input. + * @type {number} + * @attr + * @default 0 + */ + @property({ type: Number }) + public get min(): number { + //return this.#pickerContext.min; + return 0; + } + public set min(value: number) { + //this.#pickerContext.min = value; + } + + /** + * Min validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + minMessage = 'This field need more items'; + + /** + * This is a maximum amount of selected items in this input. + * @type {number} + * @attr + * @default Infinity + */ + @property({ type: Number }) + public get max(): number { + //return this.#pickerContext.max; + return Infinity; + } + public set max(value: number) { + //this.#pickerContext.max = value; + } + + /** + * Max validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + maxMessage = 'This field exceeds the allowed amount of items'; + + public get selectedIds(): Array { + //return this.#pickerContext.getSelection(); + return []; + } + public set selectedIds(ids: Array) { + //this.#pickerContext.setSelection(ids); + } + + @property() + public set value(idsString: string) { + // Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection. + this.selectedIds = splitStringToArray(idsString); + } + + @state() + private _items?: Array; + + // TODO: Create the `UmbMemberPickerContext` [LK] + //#pickerContext = new UmbMemberPickerContext(this); + + constructor() { + super(); + + // this.addValidator( + // 'rangeUnderflow', + // () => this.minMessage, + // () => !!this.min && this.#pickerContext.getSelection().length < this.min, + // ); + + // this.addValidator( + // 'rangeOverflow', + // () => this.maxMessage, + // () => !!this.max && this.#pickerContext.getSelection().length > this.max, + // ); + + // this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(','))); + // this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); + } + + protected _openPicker() { + console.log("member.openPicker"); + // this.#pickerContext.openPicker({ + // hideTreeRoot: true, + // }); + } + + protected _requestRemoveItem(item: MemberItemResponseModel){ + console.log("member.requestRemoveItem", item); + //this.#pickerContext.requestRemoveItem(item.id!); + } + + protected getFormElement() { + return undefined; + } + + render() { + return html` + ${this.#renderItems()} + ${this.#renderAddButton()} + `; + } + + #renderItems() { + if (!this._items) return; + // TODO: Add sorting. [LK] + return html`${repeat( + this._items, + (item) => item.id, + (item) => this._renderItem(item), + )} + `; + } + + #renderAddButton() { + if (this.max > 0 && this.selectedIds.length >= this.max) return; + return html``; + } + + private _renderItem(item: MemberItemResponseModel) { + if (!item.id) return; + return html` + + + + this._requestRemoveItem(item)} + label="Remove member ${item.name}" + >Remove + + + `; + } + + static styles = [ + css` + #add-button { + width: 100%; + } + `, + ]; +} + +export default UmbInputMemberElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-member': UmbInputMemberElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/members/components/input-member/input-member.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/members/members/components/input-member/input-member.stories.ts new file mode 100644 index 0000000000..7bca7fc5db --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/members/components/input-member/input-member.stories.ts @@ -0,0 +1,14 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-member.element.js'; +import type { UmbInputMemberElement } from './input-member.element.js'; + +const meta: Meta = { + title: 'Components/Inputs/Member', + component: 'umb-input-member', +}; + +export default meta; +type Story = StoryObj; +export const Overview: Story = { + args: {}, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/members/components/input-member/input-member.test.ts b/src/Umbraco.Web.UI.Client/src/packages/members/members/components/input-member/input-member.test.ts new file mode 100644 index 0000000000..7bef94720e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/members/components/input-member/input-member.test.ts @@ -0,0 +1,20 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { UmbInputMemberElement } from './input-member.element.js'; +import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; +describe('UmbInputMemberElement', () => { + let element: UmbInputMemberElement; + + beforeEach(async () => { + element = await fixture(html` `); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbInputMemberElement); + }); + + if ((window as any).__UMBRACO_TEST_RUN_A11Y_TEST) { + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); + } +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/members/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/members/index.ts index 3d76f338dd..f23e6176e5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/members/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/members/index.ts @@ -1 +1,4 @@ +import './components/index.js'; + +export * from './components/index.js'; export * from './repository/index.js';