diff --git a/packages/base/src/UI5Element.ts b/packages/base/src/UI5Element.ts index 96a4371ecf2a..b2b8a1df5e48 100644 --- a/packages/base/src/UI5Element.ts +++ b/packages/base/src/UI5Element.ts @@ -123,6 +123,7 @@ abstract class UI5Element extends HTMLElement { _inDOM: boolean; _fullyConnected: boolean; _childChangeListeners: Map; + _slotsAssignedNodes: WeakMap>; _slotChangeListeners: Map; _domRefReadyPromise: Promise & { _deferredResolve?: PromiseResolve }; _doNotSyncAttributes: Set; @@ -153,6 +154,7 @@ abstract class UI5Element extends HTMLElement { }); this._domRefReadyPromise._deferredResolve = deferredResolve; this._doNotSyncAttributes = new Set(); // attributes that are excluded from attributeChangedCallback synchronization + this._slotsAssignedNodes = new WeakMap(); // map of all nodes, slotted (directly or transitively) per component slot this._state = { ...ctor.getMetadata().getInitialState() }; @@ -396,15 +398,12 @@ abstract class UI5Element extends HTMLElement { // Listen for any invalidation on the child if invalidateOnChildChange is true or an object (ignore when false or not set) if (instanceOfUI5Element(child) && slotData.invalidateOnChildChange) { const childChangeListener = this._getChildChangeListener(slotName); - - if (childChangeListener) { - child.attachInvalidate.call(child, childChangeListener); - } + child.attachInvalidate.call(child, childChangeListener); } // Listen for the slotchange event if the child is a slot itself if (child instanceof HTMLSlotElement) { - this._attachSlotChange(child, slotName); + this._attachSlotChange(child, slotName, !!slotData.invalidateOnChildChange); } const propertyName = slotData.propertyName || slotName; @@ -465,10 +464,7 @@ abstract class UI5Element extends HTMLElement { children.forEach(child => { if (instanceOfUI5Element(child)) { const childChangeListener = this._getChildChangeListener(slotName); - - if (childChangeListener) { - child.detachInvalidate.call(child, childChangeListener); - } + child.detachInvalidate.call(child, childChangeListener); } if (child instanceof HTMLSlotElement) { @@ -659,11 +655,34 @@ abstract class UI5Element extends HTMLElement { /** * @private */ - _attachSlotChange(child: HTMLSlotElement, slotName: string) { + _attachSlotChange(slot: HTMLSlotElement, slotName: string, invalidateOnChildChange: boolean) { const slotChangeListener = this._getSlotChangeListener(slotName); - if (slotChangeListener) { - child.addEventListener("slotchange", slotChangeListener); - } + slot.addEventListener("slotchange", (e: Event) => { + slotChangeListener.call(slot, e); + + if (invalidateOnChildChange) { + // Detach listeners for UI5 Elements that used to be in this slot + const previousChildren = this._slotsAssignedNodes.get(slot); + if (previousChildren) { + previousChildren.forEach(child => { + if (instanceOfUI5Element(child)) { + const childChangeListener = this._getChildChangeListener(slotName); + child.detachInvalidate.call(child, childChangeListener); + } + }); + } + + // Attach listeners for UI5 Elements that are now in this slot + const newChildren = getSlottedNodesList([slot]); + this._slotsAssignedNodes.set(slot, newChildren); + newChildren.forEach(child => { + if (instanceOfUI5Element(child)) { + const childChangeListener = this._getChildChangeListener(slotName); + child.attachInvalidate.call(child, childChangeListener); + } + }); + } + }); } /** diff --git a/packages/compat/src/i18n/messagebundle_cs.properties b/packages/compat/src/i18n/messagebundle_cs.properties index 0b26fb8d93e1..bd4a70f5114f 100644 --- a/packages/compat/src/i18n/messagebundle_cs.properties +++ b/packages/compat/src/i18n/messagebundle_cs.properties @@ -7,7 +7,7 @@ TABLE_ROW_POSITION={0} z {1} TABLE_GROUP_ROW_ARIA_LABEL=Řádka hlavičky skupiny -ARIA_LABEL_ROW_SELECTION=Výběr položek +ARIA_LABEL_ROW_SELECTION=Výběr položky ARIA_LABEL_SELECT_ALL_CHECKBOX=Vybrat všechny řádky diff --git a/packages/main/src/MultiInput.ts b/packages/main/src/MultiInput.ts index 2e772e88fce3..2f055727537f 100644 --- a/packages/main/src/MultiInput.ts +++ b/packages/main/src/MultiInput.ts @@ -244,11 +244,16 @@ class MultiInput extends Input implements IFormInputElement { return this._focusFirstToken(e); } - if (isLeft(e) || isBackSpace(e)) { + if (isLeft(e)) { this._skipOpenSuggestions = true; return this._handleLeft(e); } + if (isBackSpace(e)) { + this._skipOpenSuggestions = true; + return this._handleBackspace(e); + } + this._skipOpenSuggestions = false; if (isShow(e)) { @@ -283,6 +288,21 @@ class MultiInput extends Input implements IFormInputElement { } } + _handleBackspace(e: KeyboardEvent) { + const cursorPosition = this.getDomRef()!.querySelector(`input`)!.selectionStart; + const selectionEnd = this.getDomRef()!.querySelector(`input`)!.selectionEnd; + const isValueSelected = cursorPosition === 0 && selectionEnd === this.value.length; + const tokens = this.tokens; + const lastToken = tokens.length && tokens[tokens.length - 1]; + + // selectionStart property applies only to inputs of types text, search, URL, tel, and password + if ((!this.value || (this.value && cursorPosition === 0 && !isValueSelected)) && lastToken) { + e.preventDefault(); + lastToken.focus(); + this.tokenizer._itemNav.setCurrentItem(lastToken); + } + } + _focusFirstToken(e: KeyboardEvent) { const tokens = this.tokens; const firstToken = tokens.length && tokens[0]; diff --git a/packages/main/src/ToolbarButton.hbs b/packages/main/src/ToolbarButton.hbs index a9f596fe4718..3c8ad9f04fab 100644 --- a/packages/main/src/ToolbarButton.hbs +++ b/packages/main/src/ToolbarButton.hbs @@ -3,7 +3,7 @@ id="{{this.id}}" style="{{this.styles}}" icon="{{this.icon}}" - ?icon-end="{{this.iconEnd}}" + end-icon="{{this.endIcon}}" tooltip="{{this.tooltip}}" accessible-name="{{this.accessibleName}}" accessible-name-ref="{{this.accessibleNameRef}}" diff --git a/packages/main/src/ToolbarButton.ts b/packages/main/src/ToolbarButton.ts index f8f2d2ff7901..8babeb1903ce 100644 --- a/packages/main/src/ToolbarButton.ts +++ b/packages/main/src/ToolbarButton.ts @@ -76,12 +76,20 @@ class ToolbarButton extends ToolbarItem { icon!: string; /** - * Defines whether the icon should be displayed after the component text. - * @default false + * Defines the icon, displayed as graphical element within the component after the button text. + * + * **Note:** It is highly recommended to use `endIcon` property only together with `icon` and/or `text` properties. + * Usage of `endIcon` only should be avoided. + * + * The SAP-icons font provides numerous options. + * + * Example: + * See all the available icons within the [Icon Explorer](https://sdk.openui5.org/test-resources/sap/m/demokit/iconExplorer/webapp/index.html). + * @default "" * @public */ - @property({ type: Boolean }) - iconEnd!: boolean; + @property() + endIcon!: string; /** * Defines the tooltip of the component. diff --git a/packages/main/src/ToolbarPopoverButton.hbs b/packages/main/src/ToolbarPopoverButton.hbs index 4296eeadc8ce..67af9c4df026 100644 --- a/packages/main/src/ToolbarPopoverButton.hbs +++ b/packages/main/src/ToolbarPopoverButton.hbs @@ -1,6 +1,6 @@ diff --git a/packages/main/test/specs/MultiInput.spec.js b/packages/main/test/specs/MultiInput.spec.js index b6c658fc6077..d9920e836c1d 100644 --- a/packages/main/test/specs/MultiInput.spec.js +++ b/packages/main/test/specs/MultiInput.spec.js @@ -1,5 +1,8 @@ import { assert } from "chai"; +const isMacOS = process.platform === 'darwin'; +const keyCtrlToPress = isMacOS ? 'Command' : 'Control'; + const isListItemFocused = async (listItem) => { return await browser.execute(el => { const pseudoElementStyle = window.getComputedStyle(el, ":after"); @@ -523,6 +526,40 @@ describe("Keyboard handling", () => { assert.ok(await lastToken.getProperty("focused"), "The last token is focused on Backspace"); }); + it("should focus token last token when caret is at the beginning of the value", async () => { + const input = await browser.$("#two-tokens"); + const innerInput = await input.shadow$("input"); + const lastToken = await browser.$("#two-tokens ui5-token#secondToken"); + + // Act + await innerInput.click(); + await browser.keys("ArrowLeft"); + await browser.keys("ArrowLeft"); + await browser.keys("ArrowLeft"); + await browser.keys("Backspace"); + + assert.ok(await lastToken.getProperty("focused"), "The last token is focused on Backspace"); + }); + + it("should delete value on backspace", async () => { + const input = await browser.$("#two-tokens"); + const innerInput = await input.shadow$("input"); + const lastToken = await browser.$("#two-tokens ui5-token#secondToken"); + + // Act + await innerInput.click(); + await browser.keys([keyCtrlToPress, "a"]); + await browser.keys("Backspace"); + + // Assert + assert.strictEqual(await input.getProperty("value"), "", "Value is deleted on Backspace"); + + await browser.keys("Backspace"); + + assert.notOk(await input.getProperty("focused"), "The input loses focus on Backspace"); + assert.ok(await lastToken.getProperty("focused"), "The last token is focused on Backspace"); + }); + it("should delete token on backspace", async () => { const input = await browser.$("#two-tokens"); const innerInput = await input.shadow$("input"); diff --git a/packages/main/test/specs/ToolbarButton.spec.js b/packages/main/test/specs/ToolbarButton.spec.js index ad8894870083..52219924bcc6 100644 --- a/packages/main/test/specs/ToolbarButton.spec.js +++ b/packages/main/test/specs/ToolbarButton.spec.js @@ -12,14 +12,14 @@ describe("Toolbar general interaction", () => { const buttonDesign = await button.getAttribute("design"); const buttonDisabled = await button.getAttribute("disabled"); const buttonIcon = await button.getAttribute("icon"); - const buttonIconEnd = await button.getAttribute("icon-end"); + const buttonEndIcon = await button.getAttribute("end-icon"); const buttonTooltip = await button.getAttribute("tooltip"); assert.strictEqual(buttonText, "Back", "Button text is correct"); assert.strictEqual(buttonDesign, "Emphasized", "Button design is correct"); assert.strictEqual(buttonDisabled, "true", "Button is disabled"); assert.strictEqual(buttonIcon, "sap-icon://add", "Button icon is correct"); - assert.strictEqual(buttonIconEnd, "", "Button icon-end is correct"); + assert.strictEqual(buttonEndIcon, "sap-icon://employee", "Button end-icon is correct"); assert.strictEqual(buttonTooltip, "Add", "Button tooltip is correct"); }); @@ -52,14 +52,14 @@ describe("Toolbar general interaction", () => { const buttonDesign = await popover.$("ui5-button").getAttribute("design"); const buttonDisabled = await popover.$("ui5-button").getAttribute("disabled"); const buttonIcon = await popover.$("ui5-button").getAttribute("icon"); - const buttonIconEnd = await popover.$("ui5-button").getAttribute("icon-end"); + const buttonEndIcon = await popover.$("ui5-button").getAttribute("end-icon"); const buttonTooltip = await popover.$("ui5-button").getAttribute("tooltip"); assert.strictEqual(buttonText, "Back", "Button's text is correct inside popover"); assert.strictEqual(buttonDesign, "Emphasized", "Button's design is correct inside popover"); assert.strictEqual(buttonDisabled, "true", "Button is disabled inside popover"); assert.strictEqual(buttonIcon, "sap-icon://add", "Button's icon is correct inside popover"); - assert.strictEqual(buttonIconEnd, "", "Button's icon-end is correct inside popover"); + assert.strictEqual(buttonEndIcon, "sap-icon://employee", "Button's end-icon is correct inside popover"); assert.strictEqual(buttonTooltip, "Add", "Button's tooltip is correct inside popover"); });