|undefined) {
+ if (frame !== undefined) {
+ frame.nativeElement?.addEventListener('turbo:frame-load', () => {
+ const modal = this.elementRef.nativeElement as HTMLElement;
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
+ void this.reposition(modal, this.locals.event.target as HTMLElement);
+ });
+ }
+ }
- public text = {
- created_by: this.i18n.t('js.label_created_by'),
- };
+ turboFrameSrc:string;
@Input() public alignment?:Placement = 'bottom-end';
@@ -75,32 +76,13 @@ export class WpPreviewModalComponent extends OpModalComponent implements OnInit
readonly elementRef:ElementRef,
@Inject(OpModalLocalsToken) readonly locals:OpModalLocalsMap,
readonly cdRef:ChangeDetectorRef,
- readonly i18n:I18nService,
- readonly apiV3Service:ApiV3Service,
- readonly opModalService:OpModalService,
- readonly $state:StateService,
) {
super(locals, cdRef, elementRef);
}
ngOnInit() {
super.ngOnInit();
- const { workPackageLink } = this.locals;
- const workPackageId = idFromLink(workPackageLink as string|null);
-
- this
- .apiV3Service
- .work_packages
- .id(workPackageId)
- .requireAndStream()
- .subscribe((workPackage:WorkPackageResource) => {
- this.workPackage = workPackage;
- this.cdRef.detectChanges();
-
- const modal = this.elementRef.nativeElement as HTMLElement;
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any
- void this.reposition(modal, this.locals.event.target as HTMLElement);
- });
+ this.turboFrameSrc = this.locals.turboFrameSrc as string;
}
public async reposition(element:HTMLElement, target:HTMLElement) {
@@ -125,9 +107,4 @@ export class WpPreviewModalComponent extends OpModalComponent implements OnInit
top: `${y}px`,
});
}
-
- public openStateLink(event:{ workPackageId:string; requestedState:string }) {
- const params = { workPackageId: event.workPackageId };
- void this.$state.go(event.requestedState, params);
- }
}
diff --git a/frontend/src/app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.modal.html b/frontend/src/app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.modal.html
deleted file mode 100644
index f87dd3384d32..000000000000
--- a/frontend/src/app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.modal.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/frontend/src/app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.modal.sass b/frontend/src/app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.modal.sass
deleted file mode 100644
index 2ceae2dfb059..000000000000
--- a/frontend/src/app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.modal.sass
+++ /dev/null
@@ -1,9 +0,0 @@
-@import "helpers"
-
-.op-wp-preview-modal
- position: absolute
- z-index: 5000
- min-width: 350px
- padding: 0px
- box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.25)
- pointer-events: all
\ No newline at end of file
diff --git a/lib/open_project/text_formatting/filters/mention_filter.rb b/lib/open_project/text_formatting/filters/mention_filter.rb
index f007d464ea04..9ced1b421ca8 100644
--- a/lib/open_project/text_formatting/filters/mention_filter.rb
+++ b/lib/open_project/text_formatting/filters/mention_filter.rb
@@ -75,7 +75,8 @@ def group_mention(group)
def work_package_mention(work_package)
link_to("##{work_package.id}",
work_package_path_or_url(id: work_package.id, only_path: context[:only_path]),
- class: "issue work_package preview-trigger")
+ class: "issue work_package op-hover-card--preview-trigger",
+ data: { "hover-card-url": hover_card_work_package_path(work_package.id) })
end
def class_from_mention(mention)
diff --git a/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb b/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb
index dfffcf173db3..42128b5a8733 100644
--- a/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb
+++ b/lib/open_project/text_formatting/matchers/link_handlers/work_packages.rb
@@ -66,7 +66,8 @@ def render_work_package_macro(wp_id, detailed: false)
def render_work_package_link(wp_id)
link_to("##{wp_id}",
work_package_path_or_url(id: wp_id, only_path: context[:only_path]),
- class: "issue work_package preview-trigger")
+ class: "issue work_package op-hover-card--preview-trigger",
+ data: { "hover-card-url": hover_card_work_package_path(wp_id) })
end
end
end
diff --git a/lookbook/docs/patterns/25-hover-cards.md.erb b/lookbook/docs/patterns/25-hover-cards.md.erb
new file mode 100644
index 000000000000..2bb9f2bc0a9d
--- /dev/null
+++ b/lookbook/docs/patterns/25-hover-cards.md.erb
@@ -0,0 +1,73 @@
+The HoverCard is a pattern related to the `Primer::Beta::Popover` and is used to show additional contexual information on certain kinds of resources like work packages and users. The hover card is opened by hovering over a certain trigger. When hovering outside of the card or its trigger, the popover is closed again.
+
+## Overview
+
+![Exemplary hover card](<%= image_path('lookbook/hover_card.png') %>)
+
+## Anatomy
+
+The HoverCard always consists of two basic parts:
+
+1. A trigger: That can be anything that is hoverable, like a link or a chip
+2. The actual card: A small popover that is opened directly next to the trigger. The actual content of the card depends on the type of resource it is calling.
+
+
+## Best practices
+
+**Do**
+
+- Put in a slight delay between hovering and displaying the card to avoid accidental triggering, which can be annoying.
+- Keep the content of the card simple. Only the essentials.
+
+**Don't**
+
+- Don't put additional interactive elements inside of the card. Since the popover closes as soon as you move the mouse out, users will find it frustrating if they try further interacting with it and have it keep disappearing
+- Don't put too many triggers on one page, as it can otherwise become annoying to have too many items trigger a card that blocks part of the screen
+
+## Used in
+
+- WorkPackage preview when linking via `#ID`
+- Soon: User preview when hovering the avatar
+
+## Technical notes
+
+Unfortunately, we could not easily use the `Primer::Beta::Popover` component.
+That is why, the `HoverCard` is technically an Angular modal which renders inside a `turboFrame`.
+This modal is triggered by a class called `op-hover-card--preview-trigger` which can be set in any element.
+A global event listener is registered on all elements with this class and triggers the modal when being hovered.
+Additionally, the trigger element needs to pass the URL for the `turboFrame` as a data attribute called `data-hover-card-url`.
+
+### Code structure
+
+**Angular modal**:
+```html
+
+
+
+
+
+```
+
+**Trigger**:
+```html
+
+
+ #14
+
+```
+
+**Actually rendered card content**:
+```html
+
+
+ <%= render WorkPackages::HoverCardComponent.new(id: 14) %>
+
+ %>
+```
diff --git a/lookbook/previews/open_project/work_packages/status_button_component_preview.rb b/lookbook/previews/open_project/work_packages/status_button_component_preview.rb
new file mode 100644
index 000000000000..1f05e2540ebd
--- /dev/null
+++ b/lookbook/previews/open_project/work_packages/status_button_component_preview.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module OpenProject::WorkPackages
+ # @logical_path OpenProject/WorkPackages
+ class StatusButtonComponentPreview < ViewComponent::Preview
+ # !! Currently nothing happens when changing the status!!
+ # @display min_height 400px
+ # @param readonly [Boolean]
+ # @param size [Symbol] select [small, medium, large]
+ def playground(readonly: true, size: :medium)
+ user = FactoryBot.build_stubbed(:admin)
+ render(WorkPackages::StatusButtonComponent.new(work_package: WorkPackage.visible.first,
+ user:,
+ readonly:,
+ button_arguments: { size: }))
+ end
+ end
+end
diff --git a/spec/features/work_packages/details/markdown/activity_comments_spec.rb b/spec/features/work_packages/details/markdown/activity_comments_spec.rb
index 06ba11d9d6ec..07afd80e0e7c 100644
--- a/spec/features/work_packages/details/markdown/activity_comments_spec.rb
+++ b/spec/features/work_packages/details/markdown/activity_comments_spec.rb
@@ -247,7 +247,7 @@
wp_page.expect_comment text: "Single ##{work_package2.id}"
expect(page).to have_css(".user-comment opce-macro-wp-quickinfo", count: 2)
- expect(page).to have_css(".user-comment .work-package--quickinfo.preview-trigger", count: 2)
+ expect(page).to have_css(".user-comment opce-macro-wp-quickinfo .op-hover-card--preview-trigger", count: 2)
end
end
diff --git a/spec/features/wysiwyg/macros/quicklink_macros_spec.rb b/spec/features/wysiwyg/macros/quicklink_macros_spec.rb
index d487d2ad5e12..8852bdf61b64 100644
--- a/spec/features/wysiwyg/macros/quicklink_macros_spec.rb
+++ b/spec/features/wysiwyg/macros/quicklink_macros_spec.rb
@@ -55,7 +55,7 @@
# Expect output widget
within("#content") do
expect(page).to have_link("##{work_package.id}")
- expect(page).to have_no_css(".work-package--quickinfo.preview-trigger")
+ expect(page).to have_no_css("opce-macro-wp-quickinfo .op-hover-card--preview-trigger")
end
# Edit page again
@@ -77,7 +77,7 @@
expected_macro_text = "#{work_package.type.name.upcase} ##{work_package.id}: My subject"
expect(page).to have_css("opce-macro-wp-quickinfo", text: expected_macro_text)
expect(page).to have_css("span", text: work_package.type.name.upcase)
- expect(page).to have_css(".work-package--quickinfo.preview-trigger", text: "##{work_package.id}")
+ expect(page).to have_css(".op-hover-card--preview-trigger", text: "##{work_package.id}")
expect(page).to have_css("span", text: "My subject")
end
@@ -102,7 +102,7 @@
expect(page).to have_css("opce-macro-wp-quickinfo", text: expected_macro_text)
expect(page).to have_css("span", text: work_package.status.name)
expect(page).to have_css("span", text: work_package.type.name.upcase)
- expect(page).to have_css(".work-package--quickinfo.preview-trigger", text: "##{work_package.id}")
+ expect(page).to have_css(".op-hover-card--preview-trigger", text: "##{work_package.id}")
expect(page).to have_css("span", text: "My subject")
# Dates are being rendered in two nested spans
expect(page).to have_css("span", text: "01/01/2020", count: 2)
diff --git a/spec/helpers/work_packages_helper_spec.rb b/spec/helpers/work_packages_helper_spec.rb
index a485dc669f8e..5aa1b857019c 100644
--- a/spec/helpers/work_packages_helper_spec.rb
+++ b/spec/helpers/work_packages_helper_spec.rb
@@ -158,122 +158,6 @@
end
end
- describe "#work_package_css_classes" do
- let(:statuses) { (1..5).map { |_i| build_stubbed(:status) } }
- let(:priority) { build_stubbed(:priority, is_default: true) }
- let(:status) { statuses[0] }
- let(:stub_work_package) do
- build_stubbed(:work_package,
- status:,
- priority:)
- end
-
- it "always has the work_package class" do
- expect(helper.work_package_css_classes(stub_work_package)).to include("work_package")
- end
-
- it "returns the position of the work_package's status" do
- stub_work_package.status = open_status
- allow(open_status).to receive(:position).and_return(5)
-
- expect(helper.work_package_css_classes(stub_work_package)).to include("status-5")
- end
-
- it "returns the position of the work_package's priority" do
- allow(priority).to receive(:position).and_return(5)
-
- expect(helper.work_package_css_classes(stub_work_package)).to include("priority-5")
- end
-
- it "has a closed class if the work_package is closed" do
- allow(stub_work_package).to receive(:closed?).and_return(true)
-
- expect(helper.work_package_css_classes(stub_work_package)).to include("closed")
- end
-
- it "has no closed class if the work_package is not closed" do
- allow(stub_work_package).to receive(:closed?).and_return(false)
-
- expect(helper.work_package_css_classes(stub_work_package)).not_to include("closed")
- end
-
- it "has an overdue class if the work_package is overdue" do
- allow(stub_work_package).to receive(:overdue?).and_return(true)
-
- expect(helper.work_package_css_classes(stub_work_package)).to include("overdue")
- end
-
- it "has an overdue class if the work_package is not overdue" do
- allow(stub_work_package).to receive(:overdue?).and_return(false)
-
- expect(helper.work_package_css_classes(stub_work_package)).not_to include("overdue")
- end
-
- it "has a child class if the work_package is a child" do
- allow(stub_work_package).to receive(:child?).and_return(true)
-
- expect(helper.work_package_css_classes(stub_work_package)).to include("child")
- end
-
- it "has no child class if the work_package is not a child" do
- allow(stub_work_package).to receive(:child?).and_return(false)
-
- expect(helper.work_package_css_classes(stub_work_package)).not_to include("child")
- end
-
- it "has a parent class if the work_package is a parent" do
- allow(stub_work_package).to receive(:leaf?).and_return(false)
-
- expect(helper.work_package_css_classes(stub_work_package)).to include("parent")
- end
-
- it "has no parent class if the work_package is not a parent" do
- allow(stub_work_package).to receive(:leaf?).and_return(true)
-
- expect(helper.work_package_css_classes(stub_work_package)).not_to include("parent")
- end
-
- it "has a created-by-me class if the work_package is a created by the current user" do
- stub_user = double("user", logged?: true, id: 5)
- allow(User).to receive(:current).and_return(stub_user)
- allow(stub_work_package).to receive(:author_id).and_return(5)
-
- expect(helper.work_package_css_classes(stub_work_package)).to include("created-by-me")
- end
-
- it "has no created-by-me class if the work_package is not created by the current user" do
- stub_user = double("user", logged?: true, id: 5)
- allow(User).to receive(:current).and_return(stub_user)
- allow(stub_work_package).to receive(:author_id).and_return(4)
-
- expect(helper.work_package_css_classes(stub_work_package)).not_to include("created-by-me")
- end
-
- it "has a created-by-me class if the work_package is the current user is not logged in" do
- expect(helper.work_package_css_classes(stub_work_package)).not_to include("created-by-me")
- end
-
- it "has a assigned-to-me class if the work_package is a created by the current user" do
- stub_user = double("user", logged?: true, id: 5)
- allow(User).to receive(:current).and_return(stub_user)
- allow(stub_work_package).to receive(:assigned_to_id).and_return(5)
-
- expect(helper.work_package_css_classes(stub_work_package)).to include("assigned-to-me")
- end
-
- it "has no assigned-to-me class if the work_package is not created by the current user" do
- stub_user = double("user", logged?: true, id: 5)
- allow(User).to receive(:current).and_return(stub_user)
- allow(stub_work_package).to receive(:assigned_to_id).and_return(4)
-
- expect(helper.work_package_css_classes(stub_work_package)).not_to include("assigned-to-me")
- end
-
- it "has no assigned-to-me class if the work_package is the current user is not logged in" do
- expect(helper.work_package_css_classes(stub_work_package)).not_to include("assigned-to-me")
- end
- end
-
describe "#work_packages_columns_options" do
it "returns the columns options" do
expect(helper.work_packages_columns_options)
diff --git a/spec/lib/api/v3/repositories/revision_representer_spec.rb b/spec/lib/api/v3/repositories/revision_representer_spec.rb
index 13ce2f9b2b79..edb89a94b4bf 100644
--- a/spec/lib/api/v3/repositories/revision_representer_spec.rb
+++ b/spec/lib/api/v3/repositories/revision_representer_spec.rb
@@ -95,7 +95,8 @@
id = work_package.id
str = "Totally references "
str << "##{id}"
end
diff --git a/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb b/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb
index 22968a580137..881f437cc404 100644
--- a/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb
+++ b/spec/lib/open_project/text_formatting/markdown/in_tool_links_spec.rb
@@ -267,7 +267,8 @@
let(:work_package_link) do
link_to("##{work_package.id}",
work_package_path(work_package),
- class: "issue work_package preview-trigger op-uc-link",
+ data: { "hover-card-url": hover_card_work_package_path(work_package.id) },
+ class: "issue work_package op-hover-card--preview-trigger op-uc-link",
target: "_top")
end
@@ -337,7 +338,8 @@
let(:work_package_link) do
link_to("##{work_package.id}",
work_package_path(work_package),
- class: "issue work_package preview-trigger op-uc-link",
+ data: { "hover-card-url": hover_card_work_package_path(work_package.id) },
+ class: "issue work_package op-hover-card--preview-trigger op-uc-link",
target: "_top")
end
@@ -656,7 +658,7 @@ def source_url_with_ext(**)
let(:expected) do
<<~EXPECTED
CookBook documentation
- ##{work_package.id}
+ ##{work_package.id}
[[CookBook documentation]]
diff --git a/spec/requests/api/v3/render_resource_spec.rb b/spec/requests/api/v3/render_resource_spec.rb
index c66def7ce8ea..13aa6a0dd1af 100644
--- a/spec/requests/api/v3/render_resource_spec.rb
+++ b/spec/requests/api/v3/render_resource_spec.rb
@@ -90,7 +90,8 @@
<<~HTML
Hello World! Have a look at
- ##{id}
@@ -180,7 +181,7 @@
it_behaves_like "valid response" do
let(:text) do
- "Hello *World*! Have a look at #1
\n\nwith two lines.
"
+ "Hello *World*! Have a look at #1
\n\nwith two lines.
"
end
end
end