From dab932329be8236cf40f6113e8705eeda94433b3 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Mon, 20 Jan 2025 12:46:34 +0000 Subject: [PATCH] V15: Link Picker Modal UX Flow (#17994) * Link Picker: reworked modal UX flow * Tweaked "Target" description * Link Picker modal tweaks * Localized "Reset URL" confirm modal * Awaits validation on picker change * Added `data-mark` attributes --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .../src/assets/lang/en-us.ts | 10 +- .../src/assets/lang/en.ts | 21 +- .../link-picker-modal.element.ts | 311 ++++++++++++------ 3 files changed, 223 insertions(+), 119 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index dd4d25caac..6f53e1ba9f 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -502,9 +502,9 @@ export default { copiedItemOfItems: 'Copied %0% out of %1% items', }, defaultdialogs: { - nodeNameLinkPicker: 'Link title', + nodeNameLinkPicker: 'Title', urlLinkPicker: 'Link', - anchorLinkPicker: 'Anchor / querystring', + anchorLinkPicker: 'Anchor or querystring', anchorInsert: 'Name', closeThisWindow: 'Close this window', confirmdelete: 'Are you sure you want to delete', @@ -563,14 +563,14 @@ export default { includeDescendants: 'Include descendants', theFriendliestCommunity: 'The friendliest community', linkToPage: 'Link to document', - openInNewWindow: 'Opens the linked document in a new window or tab', + openInNewWindow: 'Opens the link in a new window or tab', linkToMedia: 'Link to media', selectContentStartNode: 'Select content start node', selectMedia: 'Select media', selectMediaType: 'Select media type', selectIcon: 'Select icon', selectItem: 'Select item', - selectLink: 'Select link', + selectLink: 'Configure link', selectMacro: 'Select macro', selectContent: 'Select content', selectContentType: 'Select content type', @@ -671,7 +671,7 @@ export default { email: 'Enter your email', enterMessage: 'Enter a message...', usernameHint: 'Your username is usually your email', - anchor: '#value or ?key=value', + anchor: 'Enter an anchor or querystring, #value or ?key=value', enterAlias: 'Enter alias...', generatingAlias: 'Generating alias...', a11yCreateItem: 'Create item', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index fd555a2603..fab754e462 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -491,9 +491,9 @@ export default { copiedItemOfItems: 'Copied %0% out of %1% items', }, defaultdialogs: { - nodeNameLinkPicker: 'Link title', + nodeNameLinkPicker: 'Title', urlLinkPicker: 'Link', - anchorLinkPicker: 'Anchor / querystring', + anchorLinkPicker: 'Anchor or querystring', anchorInsert: 'Name', closeThisWindow: 'Close this window', confirmdelete: 'Are you sure you want to delete', @@ -553,7 +553,7 @@ export default { includeDescendants: 'Include descendants', theFriendliestCommunity: 'The friendliest community', linkToPage: 'Link to document', - openInNewWindow: 'Opens the linked document in a new window or tab', + openInNewWindow: 'Opens the link in a new window or tab', linkToMedia: 'Link to media', selectContentStartNode: 'Select content start node', selectEvent: 'Select event', @@ -561,7 +561,7 @@ export default { selectMediaType: 'Select media type', selectIcon: 'Select icon', selectItem: 'Select item', - selectLink: 'Select link', + selectLink: 'Configure link', selectMacro: 'Select macro', selectContent: 'Select content', selectContentType: 'Select content type', @@ -663,7 +663,7 @@ export default { email: 'Enter your email', enterMessage: 'Enter a message...', usernameHint: 'Your username is usually your email', - anchor: '#value or ?key=value', + anchor: 'Enter an anchor or querystring, #value or ?key=value', enterAlias: 'Enter alias...', generatingAlias: 'Generating alias...', a11yCreateItem: 'Create item', @@ -671,6 +671,7 @@ export default { a11yName: 'Name', rteParagraph: 'Write something amazing...', rteHeading: "What's the title?", + enterUrl: 'Enter a URL...', }, editcontenttype: { createListView: 'Create custom list view', @@ -798,6 +799,7 @@ export default { dictionary: 'Dictionary', dimensions: 'Dimensions', discard: 'Discard', + document: 'Document', down: 'Down', download: 'Download', edit: 'Edit', @@ -2671,4 +2673,13 @@ export default { toolbar_removeItem: 'Remove action', toolbar_emptyGroup: 'Empty', }, + linkPicker: { + modalSource: 'Source', + modalManual: 'Manual', + modalAnchorValidationMessage: + 'Please enter an anchor or querystring, or select a published document or media item, or manually configure the URL.', + resetUrlHeadline: 'Reset URL?', + resetUrlMessage: 'Are you sure you want to reset this URL?', + resetUrlLabel: 'Reset', + }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts index 5efc63199d..4fac996f53 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts @@ -6,38 +6,67 @@ import type { } from './link-picker-modal.token.js'; import { css, customElement, html, nothing, query, state, when } from '@umbraco-cms/backoffice/external/lit'; import { isUmbracoFolder, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; -import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; +import { umbBindToValidation, UmbValidationContext } from '@umbraco-cms/backoffice/validation'; +import { umbConfirmModal, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UmbDocumentDetailRepository } from '@umbraco-cms/backoffice/document'; import { UmbMediaDetailRepository } from '@umbraco-cms/backoffice/media'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import type { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document'; import type { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; -import type { UUIBooleanInputEvent, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UUIBooleanInputEvent, UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; type UmbInputPickerEvent = CustomEvent & { target: { value?: string } }; @customElement('umb-link-picker-modal') export class UmbLinkPickerModalElement extends UmbModalBaseElement { + #propertyLayoutOrientation: 'horizontal' | 'vertical' = 'vertical'; + + #validationContext = new UmbValidationContext(this); + + @state() + private _allowedMediaTypeUniques?: Array; + @state() private _config: UmbLinkPickerConfig = { hideAnchor: false, hideTarget: false, }; - @state() - private _allowedMediaTypeUniques?: Array; - @query('umb-input-document') private _documentPickerElement?: UmbInputDocumentElement; @query('umb-input-media') private _mediaPickerElement?: UmbInputMediaElement; - override async firstUpdated() { + @query('#link-anchor', true) + private _linkAnchorInput?: UUIInputElement; + + override connectedCallback() { + super.connectedCallback(); + if (this.data?.config) { this._config = this.data.config; } + if (this.modalContext) { + this.observe(this.modalContext.size, (size) => { + if (size === 'large' || size === 'full') { + this.#propertyLayoutOrientation = 'horizontal'; + } + }); + } + + this.#getMediaTypes(); + } + + protected override firstUpdated() { + this._linkAnchorInput?.addValidator( + 'valueMissing', + () => this.localize.term('linkPicker_modalAnchorValidationMessage'), + () => !this.value.link.url && !this.value.link.queryString, + ); + } + + async #getMediaTypes() { // Get all the media types, excluding the folders, so that files are selectable media items. const mediaTypeStructureRepository = new UmbMediaTypeStructureRepository(this); const { data: mediaTypes } = await mediaTypeStructureRepository.requestAllowedChildrenOf(null); @@ -61,7 +90,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement - ${this.#renderLinkUrlInput()} ${this.#renderLinkTitleInput()} ${this.#renderLinkTargetInput()} - ${this.#renderInternals()} + ${this.#renderLinkType()} ${this.#renderLinkAnchorInput()} ${this.#renderLinkTitleInput()} + ${this.#renderLinkTargetInput()}
@@ -153,56 +211,134 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement + ?disabled=${!this.value.link.type} + @click=${this.#onSubmit}>
`; } - #renderLinkUrlInput() { + #renderLinkType() { return html` - -
- - - - - ${when( - !this._config.hideAnchor, - () => html` - - - - `, - )} + +
+ ${this.#renderLinkTypeSelection()} ${this.#renderDocumentPicker()} ${this.#renderMediaPicker()} + ${this.#renderLinkUrlInput()} ${this.#renderLinkUrlInputReadOnly()}
`; } + #renderLinkTypeSelection() { + if (this.value.link.type) return nothing; + return html` + + + + + + `; + } + + #renderDocumentPicker() { + return html` + this.#onPickerSelection(e, 'document')}> + + `; + } + + #renderMediaPicker() { + return html` + this.#onPickerSelection(e, 'media')}> + `; + } + + #renderLinkUrlInput() { + if (this.value.link.type !== 'external') return nothing; + return html` + + ${when( + !this.value.link.unique, + () => html` +
+ + + +
+ `, + )} +
+ `; + } + + #renderLinkUrlInputReadOnly() { + if (!this.value.link.unique || !this.value.link.url) return nothing; + return html``; + } + + #renderLinkAnchorInput() { + if (this._config.hideAnchor) return nothing; + return html` + + + + `; + } + #renderLinkTitleInput() { return html` - + + -
- ${when( - !this.value.link.unique, - () => html` - - - - - `, - )} - this.#onPickerSelection(e, 'document')}> - - this.#onPickerSelection(e, 'media')}> -
-
- `; - } - static override styles = [ css` uui-box { @@ -277,15 +376,9 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement