Skip to content

Commit dcb2885

Browse files
committed
Add a command-list component
1 parent 3b25f2e commit dcb2885

File tree

6 files changed

+207
-0
lines changed

6 files changed

+207
-0
lines changed

app/assets/stylesheets/application.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
@import 'components/modal';
1313
@import 'components/autocomplete';
1414
@import 'components/animated_badge';
15+
@import 'components/command_list';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.command-list {
2+
.command-list-button {
3+
min-width: 170px;
4+
}
5+
6+
.modal-dialog {
7+
--bs-modal-width: 600px;
8+
--bs-modal-margin: 5rem;
9+
--bs-modal-padding: 1.5rem;
10+
}
11+
12+
.list-group-item {
13+
color: $navbar-light-color;
14+
15+
&.active {
16+
color: white;
17+
}
18+
}
19+
20+
.command-list-footer {
21+
color: $navbar-light-color;
22+
}
23+
24+
.command-list-icon {
25+
border: 1px outset $navbar-light-color;
26+
}
27+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
# frozen_literal_string: true
4+
5+
class CommandListComponentPreview < Lookbook::Preview
6+
include LookbookHelper
7+
8+
# @source ../../../app/views/components/command_lists/_themes.html.erb
9+
def themes
10+
render 'components/command_lists/themes'
11+
end
12+
end
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
export default class CommandListController extends Controller {
4+
static targets = ['list', 'metaKey', "searchField", "listGroup", "listItem"];
5+
6+
connect() {
7+
this.modal = new bootstrap.Modal(this.listTarget, {})
8+
this.children = Array.from(this.listGroupTargets[0].children);
9+
if (this.listItemTargets.length > 0) {
10+
this.switchActive(this.listItemTargets[0]);
11+
}
12+
this.metaKeyTarget.innerText = this.getMetaKey()
13+
14+
document.addEventListener("keydown", this.handleKeydown.bind(this));
15+
}
16+
17+
disconnect() {
18+
document.removeEventListener("keydown", this.handleKeydown.bind(this));
19+
}
20+
21+
handleKeydown(event) {
22+
console.log('handling keydown', event.metaKey, event.key);
23+
if ((event.metaKey) && event.key === "k") {
24+
this.openModal();
25+
}
26+
}
27+
28+
openModal() {
29+
this.modal.show();
30+
this.searchField().focus();
31+
}
32+
33+
performCommand(event) {
34+
event.preventDefault();
35+
const searchParts = this.searchField().value.split(',').map(part => part.trim());
36+
const command = this.activeCommand.dataset.command
37+
.replace('_1_', searchParts[0])
38+
.replace('_2_', searchParts[1])
39+
.replace('_3_', searchParts[2]);
40+
if (this.activeCommand.dataset.target == "_blank") {
41+
window.open(command, '_blank');
42+
} else {
43+
Turbo.visit(command);
44+
}
45+
}
46+
47+
getMetaKey() {
48+
const userAgent = window.navigator.userAgent.toLowerCase();
49+
if (userAgent.includes(' mac ')) {
50+
return '⌘';
51+
} else {
52+
return 'CTRL';
53+
}
54+
}
55+
56+
switchActive(element) {
57+
if (this.activeCommand) {
58+
this.activeCommand.classList.remove('active');
59+
}
60+
element.classList.add('active');
61+
this.activeCommand = element;
62+
this.searchField().placeholder = element.dataset.placeholder ?? '';
63+
}
64+
65+
selectCommand(event) {
66+
event.preventDefault();
67+
this.switchActive(event.currentTarget);
68+
this.searchField().focus()
69+
}
70+
71+
previousItem() {
72+
const previous = this.listItemTargets[this.listItemTargets.indexOf(this.activeCommand) - 1];
73+
if (previous) {
74+
this.switchActive(previous)
75+
}
76+
}
77+
78+
nextItem() {
79+
const next = this.listItemTargets[this.listItemTargets.indexOf(this.activeCommand) + 1];
80+
if (next) {
81+
this.switchActive(next)
82+
}
83+
}
84+
85+
searchField() {
86+
return this.searchFieldTarget
87+
}
88+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<div class="alert alert-warning">
2+
Since this page is an iframe, you need to click here first, and then press the shortcut.
3+
</div>
4+
<%
5+
6+
command_list_actions = [
7+
{ label: 'Search on Google',
8+
command: 'https://www.google.com/search?q=_1_',
9+
placeholder: 'Search text',
10+
category: 'search',
11+
target: '_blank' },
12+
{ label: 'Go to component',
13+
command: '/lookbook/inspect/_1_/themes',
14+
placeholder: 'badge, button, avatar, etc.',
15+
category: 'link' },
16+
].group_by { |action| action[:category] }
17+
18+
%>
19+
<div data-controller="command-list" class="command-list">
20+
<a href="#" class="nav-item me-3 p-1 text-decoration-none">
21+
<div class="input-group command-list-button ms-2" data-bs-toggle="modal" data-bs-target="#command-list-modal">
22+
<span class="input-group-text" id="basic-addon1"><i class="fa fa-search"></i></span>
23+
<div class="form-control form-control-sm text-center py-2">
24+
<%= t('command_list.press') %>
25+
<kbd class="command-list-icon" data-command-list-target="metaKey"></kbd> + <kbd class="command-list-icon">K</kbd>
26+
</div>
27+
</div>
28+
</a>
29+
30+
<div id="command-list-modal" class="modal" tabindex="-1" data-command-list-target="list">
31+
<div class="modal-dialog">
32+
<div class="modal-content">
33+
<div class="modal-body">
34+
<div class="input-group mb-3">
35+
<span class="input-group-text" id="basic-addon1"><i class="fa fa-search"></i></span>
36+
<input autocomplete="off"
37+
type="text"
38+
class="form-control"
39+
data-command-list-target="searchField"
40+
data-action="keydown.up->command-list#previousItem
41+
keydown.enter->command-list#performCommand
42+
keydown.down->command-list#nextItem">
43+
</div>
44+
45+
<div class="list-group" data-command-list-target="listGroup">
46+
<% command_list_actions.each do |category, actions| %>
47+
<div class="form-label mt-2 fw-medium"><%= t("command_list.#{category}") %></div>
48+
<% actions.each do |action| %>
49+
<a class="list-group-item" href="#"
50+
data-action="click->command-list#selectCommand"
51+
data-command-list-target="listItem"
52+
data-placeholder="<%= action[:placeholder] || '' %>"
53+
data-command="<%= action[:command] %>"
54+
data-target="<%= action[:target] || '' %>">
55+
<%= action[:label] %>
56+
</a>
57+
<% end %>
58+
<% end %>
59+
</div>
60+
61+
</div>
62+
<div class="modal-footer d-flex command-list-footer">
63+
<small class="pe-1"><kbd class="command-list-icon"></kbd> <%= t('command_list.to_select') %></small>
64+
<small class="pe-1">
65+
<kbd class="command-list-icon"></kbd>
66+
<kbd class="command-list-icon"></kbd>
67+
<%= t('command_list.to_navigate') %>
68+
</small>
69+
<small class="pe-1"><kbd class="command-list-icon">ESC</kbd> <%= t('command_list.to_exit') %></small>
70+
</div>
71+
</div>
72+
</div>
73+
</div>
74+
75+
</div>

test/system/pages_test.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ def visit_preview_page(page) = visit("/lookbook/preview/#{page}")
121121
assert_selector 'button', text: 'Confirm'
122122
end
123123

124+
test 'visit the command_list page' do
125+
visit_preview_page('command_list/themes')
126+
127+
assert_selector 'div.command-list'
124128
test 'visit the modal default page' do
125129
visit_preview_page('modal/default')
126130

0 commit comments

Comments
 (0)