mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-21 09:07:49 +08:00
When navigating the composer toolbar with arrow keys, you get stuck once you hit the headings button and arrow keys cease to work in either left/right direction. You get stuck here: <img width="595" height="58" alt="image" src="https://github.com/user-attachments/assets/cebc00a5-c629-488a-8716-6a9100e7b1b1" /> The issue was that DMenu captures keyboard commands looking for a `tab` and ignoring all other keys and this was breaking the parent handler watching for arrow keys. This will now pass other keys beyond tab back to the parent, so we can nav beyond headings in the toolbar. <img width="630" height="62" alt="image" src="https://github.com/user-attachments/assets/88d96979-0651-4744-8dd7-d2f2d8abe53b" /> I've added a test to check for this "stuck" state in the future.
205 lines
5.7 KiB
Text
Vendored
205 lines
5.7 KiB
Text
Vendored
import Component from "@glimmer/component";
|
|
import { concat } from "@ember/helper";
|
|
import { on } from "@ember/modifier";
|
|
import { action } from "@ember/object";
|
|
import { getOwner } from "@ember/owner";
|
|
import { service } from "@ember/service";
|
|
import curryComponent from "ember-curry-component";
|
|
import { modifier } from "ember-modifier";
|
|
import { and } from "truth-helpers";
|
|
import DButton from "discourse/components/d-button";
|
|
import DModal from "discourse/components/d-modal";
|
|
import concatClass from "discourse/helpers/concat-class";
|
|
import { isTesting } from "discourse/lib/environment";
|
|
import DFloatBody from "float-kit/components/d-float-body";
|
|
import { MENU } from "float-kit/lib/constants";
|
|
import DMenuInstance from "float-kit/lib/d-menu-instance";
|
|
|
|
export default class DMenu extends Component {
|
|
@service site;
|
|
|
|
menuInstance = new DMenuInstance(getOwner(this), {
|
|
...this.allowedProperties,
|
|
autoUpdate: true,
|
|
listeners: true,
|
|
});
|
|
|
|
registerTrigger = modifier((domElement) => {
|
|
this.menuInstance.trigger = domElement;
|
|
this.options.onRegisterApi?.(this.menuInstance);
|
|
|
|
return () => {
|
|
this.menuInstance.destroy();
|
|
};
|
|
});
|
|
|
|
registerFloatBody = modifier((domElement) => {
|
|
this.body = domElement;
|
|
|
|
return () => {
|
|
this.body = null;
|
|
};
|
|
});
|
|
|
|
@action
|
|
teardownFloatBody() {
|
|
this.body = null;
|
|
}
|
|
|
|
@action
|
|
forwardTabToContent(event) {
|
|
// need to call the parent handler to allow arrow key navigation to siblings in toolbar contexts
|
|
const parentHandlerResult = this.args.onKeydown?.(event);
|
|
|
|
if (!this.body) {
|
|
return parentHandlerResult;
|
|
}
|
|
|
|
if (event.key === "Tab") {
|
|
event.preventDefault();
|
|
|
|
const firstFocusable = this.body.querySelector(
|
|
'button, a, input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
);
|
|
|
|
firstFocusable?.focus() || this.body.focus();
|
|
return true;
|
|
}
|
|
|
|
return parentHandlerResult;
|
|
}
|
|
|
|
get options() {
|
|
return this.menuInstance?.options ?? {};
|
|
}
|
|
|
|
get componentArgs() {
|
|
return {
|
|
close: this.menuInstance.close,
|
|
show: this.menuInstance.show,
|
|
data: this.options.data,
|
|
};
|
|
}
|
|
|
|
get triggerComponent() {
|
|
const instance = this;
|
|
const baseArguments = {
|
|
get icon() {
|
|
return instance.args.icon;
|
|
},
|
|
get translatedLabel() {
|
|
return instance.args.label;
|
|
},
|
|
get translatedAriaLabel() {
|
|
return instance.args.ariaLabel;
|
|
},
|
|
get translatedTitle() {
|
|
return instance.args.title;
|
|
},
|
|
get disabled() {
|
|
return instance.args.disabled;
|
|
},
|
|
get isLoading() {
|
|
return instance.args.isLoading;
|
|
},
|
|
};
|
|
|
|
return (
|
|
this.args.triggerComponent ||
|
|
curryComponent(DButton, baseArguments, getOwner(this))
|
|
);
|
|
}
|
|
|
|
get allowedProperties() {
|
|
const properties = {};
|
|
for (const [key, value] of Object.entries(MENU.options)) {
|
|
properties[key] = this.args[key] ?? value;
|
|
}
|
|
return properties;
|
|
}
|
|
|
|
<template>
|
|
<this.triggerComponent
|
|
{{this.registerTrigger}}
|
|
class={{concatClass
|
|
"fk-d-menu__trigger"
|
|
(if this.menuInstance.expanded "-expanded")
|
|
(concat this.options.identifier "-trigger")
|
|
@triggerClass
|
|
@class
|
|
}}
|
|
id={{this.menuInstance.id}}
|
|
data-identifier={{this.options.identifier}}
|
|
data-trigger
|
|
aria-expanded={{if this.menuInstance.expanded "true" "false"}}
|
|
{{on "keydown" this.forwardTabToContent}}
|
|
@componentArgs={{this.componentArgs}}
|
|
...attributes
|
|
>
|
|
{{#if (has-block "trigger")}}
|
|
{{yield this.componentArgs to="trigger"}}
|
|
{{/if}}
|
|
</this.triggerComponent>
|
|
|
|
{{#if this.menuInstance.expanded}}
|
|
{{#if (and this.site.mobileView this.options.modalForMobile)}}
|
|
<DModal
|
|
@closeModal={{this.menuInstance.close}}
|
|
@hideHeader={{true}}
|
|
@autofocus={{this.options.autofocus}}
|
|
class={{concatClass
|
|
"fk-d-menu-modal"
|
|
(concat this.options.identifier "-content")
|
|
@contentClass
|
|
@class
|
|
}}
|
|
@inline={{(isTesting)}}
|
|
data-identifier={{this.options.identifier}}
|
|
data-content
|
|
>
|
|
<div class="fk-d-menu-modal__grip" aria-hidden="true"></div>
|
|
{{#if (has-block)}}
|
|
{{yield this.componentArgs}}
|
|
{{else if (has-block "content")}}
|
|
{{yield this.componentArgs to="content"}}
|
|
{{else if this.options.component}}
|
|
<this.options.component
|
|
@data={{this.options.data}}
|
|
@close={{this.menuInstance.close}}
|
|
/>
|
|
{{else if this.options.content}}
|
|
{{this.options.content}}
|
|
{{/if}}
|
|
</DModal>
|
|
{{else}}
|
|
<DFloatBody
|
|
@instance={{this.menuInstance}}
|
|
@trapTab={{this.options.trapTab}}
|
|
@mainClass={{concatClass
|
|
"fk-d-menu"
|
|
(concat this.options.identifier "-content")
|
|
@class
|
|
@contentClass
|
|
}}
|
|
@innerClass="fk-d-menu__inner-content"
|
|
@role="dialog"
|
|
@inline={{this.options.inline}}
|
|
{{this.registerFloatBody}}
|
|
>
|
|
{{#if (has-block)}}
|
|
{{yield this.componentArgs}}
|
|
{{else if (has-block "content")}}
|
|
{{yield this.componentArgs to="content"}}
|
|
{{else if this.options.component}}
|
|
<this.options.component
|
|
@data={{this.options.data}}
|
|
@close={{this.menuInstance.close}}
|
|
/>
|
|
{{else if this.options.content}}
|
|
{{this.options.content}}
|
|
{{/if}}
|
|
</DFloatBody>
|
|
{{/if}}
|
|
{{/if}}
|
|
</template>
|
|
}
|