diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e1179888a0..20c37259500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Pages no longer show breadcrumbs when the only one would be the current page, such as the Dashboard. ([#20612](https://github.com/craftcms/cms/pull/20612)) - Fields’ translation indicators now show their descriptions when pressed, including on touch devices, rather than on hover. ([#20612](https://github.com/craftcms/cms/pull/20612)) - Fixed a bug where focus was lost after choosing elements in a relation field’s selector modal, or canceling it. Focus now returns to the field’s add button, or to the chosen element when the field is full. ([#20653](https://github.com/craftcms/cms/pull/20653)) +- Fixed a bug where focus was lost after choosing elements in an element select input that became full, such as one in a plugin’s settings, rather than moving to the last element’s action menu. ([#20659](https://github.com/craftcms/cms/pull/20659)) - Fixed a bug where typing in fields, especially Markdown fields, lagged on entries with many fields. ([#20637](https://github.com/craftcms/cms/issues/20637)) - Fixed a bug where the user menu showed a “Permissions” item on Craft Solo, which led to a 403 error. ([#20621](https://github.com/craftcms/cms/issues/20621), [#20635](https://github.com/craftcms/cms/pull/20635)) - Fixed a bug where control panel forms could show the wrong fields after a failed save, such as the Base URL field on a new site whose “This site has its own base URL” setting was turned off. ([#20632](https://github.com/craftcms/cms/pull/20632), [#20636](https://github.com/craftcms/cms/pull/20636)) diff --git a/resources/js/modules/element-select-input/base-element-select-input.test.ts b/resources/js/modules/element-select-input/base-element-select-input.test.ts index e2ee4ac495c..e35f94934e3 100644 --- a/resources/js/modules/element-select-input/base-element-select-input.test.ts +++ b/resources/js/modules/element-select-input/base-element-select-input.test.ts @@ -53,6 +53,16 @@ it('reads its chips and hides Add once the limit is reached', () => { expect(addButton().classList.contains('hidden')).toBe(true); }); +it("moves focus to the last chip's action menu once the field is full", () => { + const input = boot({limit: 2}); + const invoker = input + .getElementById(2)! + .querySelector('craft-action-menu > [slot="invoker"]'); + + expect(invoker).not.toBeNull(); + expect(input.getNextLogicalFocusElement()).toBe(invoker); +}); + it('removes a chip from the value and frees its slot', async () => { const input = boot({limit: 2}); const change = vi.fn(); diff --git a/resources/js/modules/element-select-input/base-element-select-input.ts b/resources/js/modules/element-select-input/base-element-select-input.ts index 504737df0ec..6f1aaac712b 100644 --- a/resources/js/modules/element-select-input/base-element-select-input.ts +++ b/resources/js/modules/element-select-input/base-element-select-input.ts @@ -562,7 +562,10 @@ export class BaseElementSelectInput extends Base } getLastActionBtn(): HTMLElement | null { - const buttons = this.container.querySelectorAll('.action-btn'); + // A chip's `craft-action-menu` invoker, or an old disclosure menu trigger + const buttons = this.container.querySelectorAll( + 'craft-action-menu > [slot="invoker"], .action-btn' + ); return buttons[buttons.length - 1] ?? null; } diff --git a/yii2-adapter/resources/js/element-select-input/base-element-select-input.test.ts b/yii2-adapter/resources/js/element-select-input/base-element-select-input.test.ts index 0ce9bd75e8e..0b92dc6efba 100644 --- a/yii2-adapter/resources/js/element-select-input/base-element-select-input.test.ts +++ b/yii2-adapter/resources/js/element-select-input/base-element-select-input.test.ts @@ -166,6 +166,26 @@ it("moves elements from their reorder buttons' menus", () => { ); }); +it("moves focus to the last chip's action menu once the field is full", () => { + class UninitializedElementSelectInput extends BaseElementSelectInput {} + + const input = new UninitializedElementSelectInput(); + const chip = (id: number) => + `
  • `; + const container = document.createElement('div'); + container.innerHTML = `
      ${chip(1)}${chip(2)}
    `; + input.$container = jquery(container); + input.$elements = input.$container.find('.element'); + input.settings = {allowAdd: true, limit: 2} as BaseElementSelectInputSettings; + + const $focusTarget = input.getNextLogicalFocusElement(); + + expect($focusTarget.length).toBe(1); + expect($focusTarget[0]).toBe( + container.querySelector('[data-id="2"] [slot="invoker"]') + ); +}); + /** * An instance with just enough stubbed to observe the order of * `onModalSelect()`'s side effects — the real method wants jQuery, garnish and diff --git a/yii2-adapter/resources/js/element-select-input/base-element-select-input.ts b/yii2-adapter/resources/js/element-select-input/base-element-select-input.ts index ed8504bd970..01a3ccc5f82 100644 --- a/yii2-adapter/resources/js/element-select-input/base-element-select-input.ts +++ b/yii2-adapter/resources/js/element-select-input/base-element-select-input.ts @@ -569,7 +569,10 @@ export class BaseElementSelectInput extends Base } getLastActionBtn(): any { - return this.$container.find('.action-btn').last(); + // A chip's `craft-action-menu` invoker, or an old disclosure menu trigger + return this.$container + .find('craft-action-menu > [slot="invoker"], .action-btn') + .last(); } focusLastActionBtn(): void {