mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
This PR introduces three new UI elements to Discourse codebase through an addon called "FloatKit":
- menu
- tooltip
- toast
Simple cases can be express with an API similar to DButton:
```hbs
<DTooltip
@label={{i18n "foo.bar"}}
@icon="check"
@content="Something"
/>
```
More complex cases can use blocks:
```hbs
<DTooltip>
<:trigger>
{{d-icon "check"}}
<span>{{i18n "foo.bar"}}</span>
</:trigger>
<:content>
Something
</:content>
</DTooltip>
```
You can manually show a tooltip using the `tooltip` service:
```javascript
const tooltipInstance = await this.tooltip.show(
document.querySelector(".my-span"),
options
)
// and later manually close or destroy it
tooltipInstance.close();
tooltipInstance.destroy();
// you can also just close any open tooltip through the service
this.tooltip.close();
```
The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service:
```javascript
const tooltipInstance = this.tooltip.register(
document.querySelector(".my-span"),
options
)
// when done you can destroy the instance to remove the listeners
tooltipInstance.destroy();
```
Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args:
```javascript
const tooltipInstance = await this.tooltip.show(
document.querySelector(".my-span"),
{
component: MyComponent,
data: { foo: 1 }
}
)
```
Menus are very similar to tooltips and provide the same kind of APIs:
```hbs
<DMenu @icon="plus" @label={{i18n "foo.bar"}}>
<ul>
<li>Foo</li>
<li>Bat</li>
<li>Baz</li>
</ul>
</DMenu>
```
They also support blocks:
```hbs
<DMenu>
<:trigger>
{{d-icon "plus"}}
<span>{{i18n "foo.bar"}}</span>
</:trigger>
<:content>
<ul>
<li>Foo</li>
<li>Bat</li>
<li>Baz</li>
</ul>
</:content>
</DMenu>
```
You can manually show a menu using the `menu` service:
```javascript
const menuInstance = await this.menu.show(
document.querySelector(".my-span"),
options
)
// and later manually close or destroy it
menuInstance.close();
menuInstance.destroy();
// you can also just close any open tooltip through the service
this.menu.close();
```
The service also allows you to register event listeners on a trigger, it removes the need for you to manage open/close of a tooltip started through the service:
```javascript
const menuInstance = this.menu.register(
document.querySelector(".my-span"),
options
)
// when done you can destroy the instance to remove the listeners
menuInstance.destroy();
```
Note that the service also allows you to use a custom component as content which will receive `@data` and `@close` as args:
```javascript
const menuInstance = await this.menu.show(
document.querySelector(".my-span"),
{
component: MyComponent,
data: { foo: 1 }
}
)
```
Interacting with toasts is made only through the `toasts` service.
A default component is provided (DDefaultToast) and can be used through dedicated service methods:
- this.toasts.success({ ... });
- this.toasts.warning({ ... });
- this.toasts.info({ ... });
- this.toasts.error({ ... });
- this.toasts.default({ ... });
```javascript
this.toasts.success({
data: {
title: "Foo",
message: "Bar",
actions: [
{
label: "Ok",
class: "btn-primary",
action: (componentArgs) => {
// eslint-disable-next-line no-alert
alert("Closing toast:" + componentArgs.data.title);
componentArgs.close();
},
}
]
},
});
```
You can also provide your own component:
```javascript
this.toasts.show(MyComponent, {
autoClose: false,
class: "foo",
data: { baz: 1 },
})
```
Co-authored-by: Martin Brennan <mjrbrennan@gmail.com>
Co-authored-by: Isaac Janzen <50783505+janzenisaac@users.noreply.github.com>
Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: Jarek Radosz <jradosz@gmail.com>
228 lines
7.1 KiB
Ruby
228 lines
7.1 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
RSpec.describe "React to message", type: :system do
|
||
fab!(:current_user) { Fabricate(:user, group_ids: [Group::AUTO_GROUPS[:trust_level_1]]) }
|
||
fab!(:other_user) { Fabricate(:user, group_ids: [Group::AUTO_GROUPS[:trust_level_1]]) }
|
||
fab!(:category_channel_1) { Fabricate(:category_channel) }
|
||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: category_channel_1) }
|
||
|
||
let(:chat) { PageObjects::Pages::Chat.new }
|
||
let(:channel) { PageObjects::Pages::ChatChannel.new }
|
||
|
||
before do
|
||
chat_system_bootstrap
|
||
category_channel_1.add(current_user)
|
||
category_channel_1.add(other_user)
|
||
end
|
||
|
||
context "when other user has reacted" do
|
||
fab!(:reaction_1) do
|
||
Chat::MessageReactor.new(other_user, category_channel_1).react!(
|
||
message_id: message_1.id,
|
||
react_action: :add,
|
||
emoji: "female_detective",
|
||
)
|
||
end
|
||
|
||
shared_examples "inline reactions" do
|
||
it "shows existing reactions under the message" do
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
expect(channel).to have_reaction(message_1, reaction_1.emoji)
|
||
end
|
||
|
||
it "increments when clicking it" do
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
channel.click_reaction(message_1, reaction_1.emoji)
|
||
expect(channel).to have_reaction(message_1, reaction_1.emoji, 2)
|
||
end
|
||
end
|
||
|
||
context "when desktop" do
|
||
include_examples "inline reactions"
|
||
end
|
||
|
||
context "when mobile", mobile: true do
|
||
include_examples "inline reactions"
|
||
end
|
||
end
|
||
|
||
context "when current user reacts" do
|
||
fab!(:reaction_1) do
|
||
Chat::MessageReactor.new(other_user, category_channel_1).react!(
|
||
message_id: message_1.id,
|
||
react_action: :add,
|
||
emoji: "female_detective",
|
||
)
|
||
end
|
||
|
||
context "when desktop" do
|
||
context "when using inline reaction button" do
|
||
it "adds a reaction" do
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
channel.hover_message(message_1)
|
||
find(".chat-message-react-btn").click
|
||
find(".chat-emoji-picker [data-emoji=\"grimacing\"]").click
|
||
|
||
expect(channel).to have_reaction(message_1, "grimacing")
|
||
end
|
||
|
||
context "when current user has multiple sessions" do
|
||
xit "adds reaction on each session" do
|
||
reaction = "grimacing"
|
||
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
|
||
using_session(:tab_1) do
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
end
|
||
|
||
using_session(:tab_1) do |session|
|
||
channel.hover_message(message_1)
|
||
find(".chat-message-react-btn").click
|
||
find(".chat-emoji-picker [data-emoji=\"#{reaction}\"]").click
|
||
|
||
expect(channel).to have_reaction(message_1, reaction)
|
||
|
||
session.quit
|
||
end
|
||
|
||
expect(channel).to have_reaction(message_1, "grimacing")
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when using message actions menu" do
|
||
context "when using the emoji picker" do
|
||
it "adds a reaction" do
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
channel.hover_message(message_1)
|
||
find(".chat-message-actions .react-btn").click
|
||
find(".chat-emoji-picker [data-emoji=\"nerd_face\"]").click
|
||
|
||
expect(channel).to have_reaction(message_1, reaction_1.emoji)
|
||
end
|
||
|
||
it "removes denied emojis and aliases from reactions" do
|
||
SiteSetting.emoji_deny_list = "fu"
|
||
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
channel.hover_message(message_1)
|
||
find(".chat-message-actions .react-btn").click
|
||
|
||
expect(page).to have_no_css(".chat-emoji-picker [data-emoji=\"fu\"]")
|
||
expect(page).to have_no_css(".chat-emoji-picker [data-emoji=\"middle_finger\"]")
|
||
end
|
||
end
|
||
|
||
context "when using frequent reactions" do
|
||
it "adds a reaction" do
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
channel.hover_message(message_1)
|
||
find(".chat-message-actions [data-emoji-name=\"+1\"]").click
|
||
|
||
expect(channel.message_reactions_list(message_1)).to have_css(
|
||
"[data-emoji-name=\"+1\"]",
|
||
)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when current user and another have reacted" do
|
||
fab!(:other_user) { Fabricate(:user, group_ids: [Group::AUTO_GROUPS[:trust_level_1]]) }
|
||
|
||
fab!(:reaction_1) do
|
||
Chat::MessageReactor.new(current_user, category_channel_1).react!(
|
||
message_id: message_1.id,
|
||
react_action: :add,
|
||
emoji: "female_detective",
|
||
)
|
||
end
|
||
|
||
fab!(:reaction_2) do
|
||
Chat::MessageReactor.new(other_user, category_channel_1).react!(
|
||
message_id: message_1.id,
|
||
react_action: :add,
|
||
emoji: "female_detective",
|
||
)
|
||
end
|
||
|
||
context "when removing the reaction" do
|
||
it "removes only the reaction from the current user" do
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
|
||
expect(channel).to have_reaction(message_1, "female_detective", "2")
|
||
|
||
channel.click_reaction(message_1, "female_detective")
|
||
|
||
expect(channel).to have_reaction(message_1, "female_detective", "1")
|
||
end
|
||
end
|
||
end
|
||
|
||
context "when current user has reacted" do
|
||
fab!(:reaction_1) do
|
||
Chat::MessageReactor.new(current_user, category_channel_1).react!(
|
||
message_id: message_1.id,
|
||
react_action: :add,
|
||
emoji: "female_detective",
|
||
)
|
||
end
|
||
|
||
shared_examples "inline reactions" do
|
||
it "shows existing reactions under the message" do
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
expect(channel).to have_reaction(message_1, reaction_1.emoji)
|
||
end
|
||
|
||
it "removes it when clicking it" do
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
channel.click_reaction(message_1, reaction_1.emoji)
|
||
expect(channel).to have_no_reactions(message_1)
|
||
end
|
||
end
|
||
|
||
context "when desktop" do
|
||
include_examples "inline reactions"
|
||
end
|
||
|
||
context "when mobile", mobile: true do
|
||
include_examples "inline reactions"
|
||
end
|
||
|
||
context "when receiving a duplicate reaction event" do
|
||
fab!(:user_1) { Fabricate(:user, group_ids: [Group::AUTO_GROUPS[:trust_level_1]]) }
|
||
|
||
fab!(:reaction_2) do
|
||
Chat::MessageReactor.new(user_1, category_channel_1).react!(
|
||
message_id: message_1.id,
|
||
react_action: :add,
|
||
emoji: "heart",
|
||
)
|
||
end
|
||
|
||
it "doesn’t create duplicate reactions" do
|
||
sign_in(current_user)
|
||
chat.visit_channel(category_channel_1)
|
||
|
||
Chat::Publisher.publish_reaction!(category_channel_1, message_1, "add", user_1, "heart")
|
||
channel.send_message("test") # cheap trick to ensure reaction has been processed
|
||
|
||
expect(channel).to have_reaction(message_1, reaction_2.emoji, "1")
|
||
end
|
||
end
|
||
end
|
||
end
|