yummy-guide-generic-administrate 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +31 -0
- data/app/assets/javascripts/yummy_guide_administrate/tooltips.js +214 -0
- data/app/assets/stylesheets/yummy_guide_administrate/_tooltips.scss +85 -0
- data/app/assets/stylesheets/yummy_guide_administrate/components.scss +1 -0
- data/app/helpers/yummy_guide/administrate/tooltip_helper.rb +57 -0
- data/lib/yummy_guide/administrate/engine.rb +2 -0
- data/lib/yummy_guide/administrate/version.rb +1 -1
- data/lib/yummy_guide/administrate.rb +1 -0
- data/spec/yummy_guide/administrate/tooltip_asset_spec.rb +63 -0
- data/spec/yummy_guide/administrate/tooltip_helper_spec.rb +96 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 18c6b70d901d111ee883c8217e15eb52d5f3b7d8d6c41ef95d371ac552a78afd
|
|
4
|
+
data.tar.gz: f6de376c7ebe6876384278c8e4993ceb23485267e30c6bcedc1087b26715e278
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16d507455a42dd056a00470296f2387ad9725f7c2fa89e9a92b42abbe825b6063eb48d34bd91e7ba468cea86458b0baaf8d6a92f3d8185cd80395d901619effc
|
|
7
|
+
data.tar.gz: 99b43545b341e94587d5f59ed3c2ab85a70759806c3048d37bc587ff25ec2c56bcd376ea68ee5b7f38fcc3d0f3368db9ae5f55a8d567e16a4cfc4785666e8eaf
|
data/README.md
CHANGED
|
@@ -43,6 +43,8 @@ bundle install
|
|
|
43
43
|
- datetime フィルターや checkbox group の組み立てを補助する helper
|
|
44
44
|
- `YummyGuide::Administrate::FilterControlsHelper`
|
|
45
45
|
- dashboard の Field 型フィルター定義から Filter ボタンとモーダルフォームを描画する helper
|
|
46
|
+
- `YummyGuide::Administrate::TooltipHelper`
|
|
47
|
+
- 管理画面内で補足説明 tooltip を描画する helper
|
|
46
48
|
- `YummyGuide::Administrate::DatetimeInputHelper`
|
|
47
49
|
- 管理画面フォーム用の date + time 入力 helper
|
|
48
50
|
- `YummyGuide::Administrate::NumberInputHelper`
|
|
@@ -58,6 +60,7 @@ bundle install
|
|
|
58
60
|
- `filter_form.js`
|
|
59
61
|
- `sticky_left_columns.js`
|
|
60
62
|
- `sticky_table_headers.js`
|
|
63
|
+
- `tooltips.js`
|
|
61
64
|
- `components.css`
|
|
62
65
|
- 共通 field
|
|
63
66
|
- `YummyGuide::Administrate::Fields::JsonPrettyField`
|
|
@@ -109,12 +112,15 @@ class Admin::ApplicationController < Administrate::ApplicationController
|
|
|
109
112
|
helper YummyGuide::Administrate::FilterControlsHelper
|
|
110
113
|
helper YummyGuide::Administrate::FilterFormHelper
|
|
111
114
|
helper YummyGuide::Administrate::NumberInputHelper
|
|
115
|
+
helper YummyGuide::Administrate::TooltipHelper
|
|
112
116
|
end
|
|
113
117
|
```
|
|
114
118
|
|
|
115
119
|
`NumberInputHelper` は `Administrate::ApplicationController` の view helper として
|
|
116
120
|
自動適用されます。`Administrate::ApplicationController` を継承しない独自 admin
|
|
117
121
|
controller で同じ挙動が必要な場合だけ、上記のように明示的に読み込んでください。
|
|
122
|
+
`TooltipHelper` も `Administrate::ApplicationController` の view helper として
|
|
123
|
+
自動適用されます。
|
|
118
124
|
|
|
119
125
|
### Collection partial
|
|
120
126
|
|
|
@@ -461,6 +467,30 @@ Admin/Administrate 画面では、`number_field` / `number_field_tag` を
|
|
|
461
467
|
`range_field` / `range_field_tag` は対象外で、従来どおり `type="range"` として描画
|
|
462
468
|
されます。raw HTML の `<input type="number">` は helper を通らないため対象外です。
|
|
463
469
|
|
|
470
|
+
### Tooltip helper
|
|
471
|
+
|
|
472
|
+
ラベルやボタンの横に補足説明用の tooltip アイコンを表示する場合は、
|
|
473
|
+
`admin_tooltip` を使います。PC では hover / focus 時に表示し、モバイルではタップで
|
|
474
|
+
表示・非表示を切り替えます。
|
|
475
|
+
|
|
476
|
+
```erb
|
|
477
|
+
<%= f.label :published_at %>
|
|
478
|
+
<%= admin_tooltip("公開日時を過ぎると公開ページに表示されます。") %>
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
説明本文に改行などの HTML を含める場合は block を渡します。`text` 引数と block を
|
|
482
|
+
同時に指定した場合は block が優先されます。
|
|
483
|
+
|
|
484
|
+
```erb
|
|
485
|
+
<%= admin_tooltip do %>
|
|
486
|
+
Add - 調整レコードを追加表示するようにします。<br>
|
|
487
|
+
Only - 調整レコードのみを表示します。
|
|
488
|
+
<% end %>
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
block を使わない場合、tooltip 本文はテキストとして扱われ、HTML は描画しません。
|
|
492
|
+
見た目と表示制御には `components.css` と `tooltips.js` の読み込みが必要です。
|
|
493
|
+
|
|
464
494
|
### Asset の読み込み
|
|
465
495
|
|
|
466
496
|
この engine の asset はホストアプリ側で明示的に読み込んでください。
|
|
@@ -473,6 +503,7 @@ Admin/Administrate 画面では、`number_field` / `number_field_tag` を
|
|
|
473
503
|
//= require yummy_guide_administrate/filter_form
|
|
474
504
|
//= require yummy_guide_administrate/sticky_left_columns
|
|
475
505
|
//= require yummy_guide_administrate/sticky_table_headers
|
|
506
|
+
//= require yummy_guide_administrate/tooltips
|
|
476
507
|
```
|
|
477
508
|
|
|
478
509
|
```scss
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
var TRIGGER_SELECTOR = "[data-admin-tooltip-trigger='true']";
|
|
3
|
+
var TOOLTIP_ID = "admin-tooltip";
|
|
4
|
+
var TOOLTIP_CLASS = "admin-tooltip";
|
|
5
|
+
var VISIBLE_CLASS = "admin-tooltip--visible";
|
|
6
|
+
var MOBILE_MEDIA_QUERY = "(max-width: 767px)";
|
|
7
|
+
var EDGE_GAP = 8;
|
|
8
|
+
var OFFSET = 8;
|
|
9
|
+
var activeTrigger = null;
|
|
10
|
+
var hoveredTrigger = null;
|
|
11
|
+
var focusedTrigger = null;
|
|
12
|
+
var tooltipElement = null;
|
|
13
|
+
|
|
14
|
+
function isMobile() {
|
|
15
|
+
return window.matchMedia && window.matchMedia(MOBILE_MEDIA_QUERY).matches;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function closestTrigger(target) {
|
|
19
|
+
if (!target || !target.closest) return null;
|
|
20
|
+
|
|
21
|
+
return target.closest(TRIGGER_SELECTOR);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function clamp(value, min, max) {
|
|
25
|
+
return Math.min(Math.max(value, min), max);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getTooltipElement() {
|
|
29
|
+
if (tooltipElement) return tooltipElement;
|
|
30
|
+
|
|
31
|
+
tooltipElement = document.createElement("div");
|
|
32
|
+
tooltipElement.id = TOOLTIP_ID;
|
|
33
|
+
tooltipElement.className = TOOLTIP_CLASS;
|
|
34
|
+
tooltipElement.setAttribute("role", "tooltip");
|
|
35
|
+
document.body.appendChild(tooltipElement);
|
|
36
|
+
|
|
37
|
+
return tooltipElement;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tooltipContent(trigger) {
|
|
41
|
+
var contentId = trigger.getAttribute("data-admin-tooltip-content-id");
|
|
42
|
+
var template = contentId ? document.getElementById(contentId) : null;
|
|
43
|
+
if (template) {
|
|
44
|
+
return {
|
|
45
|
+
html: template.innerHTML,
|
|
46
|
+
text: ""
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var text = trigger.getAttribute("data-admin-tooltip-text") || "";
|
|
51
|
+
return {
|
|
52
|
+
html: "",
|
|
53
|
+
text: text.trim() ? text : ""
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setTriggerState(trigger, expanded) {
|
|
58
|
+
if (!trigger) return;
|
|
59
|
+
|
|
60
|
+
trigger.setAttribute("aria-expanded", expanded ? "true" : "false");
|
|
61
|
+
|
|
62
|
+
if (expanded) {
|
|
63
|
+
trigger.setAttribute("aria-describedby", TOOLTIP_ID);
|
|
64
|
+
} else {
|
|
65
|
+
trigger.removeAttribute("aria-describedby");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function positionTooltip() {
|
|
70
|
+
if (!activeTrigger || !tooltipElement || !tooltipElement.classList.contains(VISIBLE_CLASS)) return;
|
|
71
|
+
|
|
72
|
+
var targetRect = activeTrigger.getBoundingClientRect();
|
|
73
|
+
tooltipElement.style.left = "0px";
|
|
74
|
+
tooltipElement.style.top = "0px";
|
|
75
|
+
|
|
76
|
+
var tooltipRect = tooltipElement.getBoundingClientRect();
|
|
77
|
+
var viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
78
|
+
var viewportHeight = document.documentElement.clientHeight || window.innerHeight;
|
|
79
|
+
var top = targetRect.top - tooltipRect.height - OFFSET;
|
|
80
|
+
var placement = "top";
|
|
81
|
+
|
|
82
|
+
if (top < EDGE_GAP) {
|
|
83
|
+
top = targetRect.bottom + OFFSET;
|
|
84
|
+
placement = "bottom";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
top = clamp(top, EDGE_GAP, Math.max(EDGE_GAP, viewportHeight - tooltipRect.height - EDGE_GAP));
|
|
88
|
+
|
|
89
|
+
var left = targetRect.left + (targetRect.width / 2) - (tooltipRect.width / 2);
|
|
90
|
+
left = clamp(left, EDGE_GAP, Math.max(EDGE_GAP, viewportWidth - tooltipRect.width - EDGE_GAP));
|
|
91
|
+
|
|
92
|
+
var arrowMax = Math.max(10, tooltipRect.width - 10);
|
|
93
|
+
var arrowLeft = clamp(targetRect.left + (targetRect.width / 2) - left - 4, 10, arrowMax);
|
|
94
|
+
|
|
95
|
+
tooltipElement.style.left = Math.round(left) + "px";
|
|
96
|
+
tooltipElement.style.top = Math.round(top) + "px";
|
|
97
|
+
tooltipElement.style.setProperty("--admin-tooltip-arrow-left", Math.round(arrowLeft) + "px");
|
|
98
|
+
tooltipElement.setAttribute("data-admin-tooltip-placement", placement);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function showTooltip(trigger) {
|
|
102
|
+
var content = tooltipContent(trigger);
|
|
103
|
+
if (!content.html && !content.text) return;
|
|
104
|
+
|
|
105
|
+
if (activeTrigger && activeTrigger !== trigger) {
|
|
106
|
+
hideTooltip();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
activeTrigger = trigger;
|
|
110
|
+
tooltipElement = getTooltipElement();
|
|
111
|
+
if (content.html) {
|
|
112
|
+
tooltipElement.innerHTML = content.html;
|
|
113
|
+
} else {
|
|
114
|
+
tooltipElement.textContent = content.text;
|
|
115
|
+
}
|
|
116
|
+
tooltipElement.classList.add(VISIBLE_CLASS);
|
|
117
|
+
setTriggerState(trigger, true);
|
|
118
|
+
|
|
119
|
+
if (window.requestAnimationFrame) {
|
|
120
|
+
window.requestAnimationFrame(positionTooltip);
|
|
121
|
+
} else {
|
|
122
|
+
positionTooltip();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hideTooltip(trigger) {
|
|
127
|
+
if (trigger && activeTrigger !== trigger) return;
|
|
128
|
+
|
|
129
|
+
setTriggerState(activeTrigger, false);
|
|
130
|
+
|
|
131
|
+
if (tooltipElement) {
|
|
132
|
+
tooltipElement.classList.remove(VISIBLE_CLASS);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
activeTrigger = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toggleTooltip(trigger) {
|
|
139
|
+
if (activeTrigger === trigger && tooltipElement && tooltipElement.classList.contains(VISIBLE_CLASS)) {
|
|
140
|
+
hideTooltip(trigger);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
showTooltip(trigger);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
document.addEventListener("mouseover", function(event) {
|
|
148
|
+
var trigger = closestTrigger(event.target);
|
|
149
|
+
if (!trigger || isMobile() || (event.relatedTarget && trigger.contains(event.relatedTarget))) return;
|
|
150
|
+
|
|
151
|
+
hoveredTrigger = trigger;
|
|
152
|
+
showTooltip(trigger);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
document.addEventListener("mouseout", function(event) {
|
|
156
|
+
var trigger = closestTrigger(event.target);
|
|
157
|
+
if (!trigger || isMobile() || (event.relatedTarget && trigger.contains(event.relatedTarget))) return;
|
|
158
|
+
|
|
159
|
+
if (hoveredTrigger === trigger) {
|
|
160
|
+
hoveredTrigger = null;
|
|
161
|
+
}
|
|
162
|
+
if (focusedTrigger === trigger) return;
|
|
163
|
+
|
|
164
|
+
hideTooltip(trigger);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
document.addEventListener("focusin", function(event) {
|
|
168
|
+
var trigger = closestTrigger(event.target);
|
|
169
|
+
if (!trigger || isMobile()) return;
|
|
170
|
+
|
|
171
|
+
focusedTrigger = trigger;
|
|
172
|
+
showTooltip(trigger);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
document.addEventListener("focusout", function(event) {
|
|
176
|
+
var trigger = closestTrigger(event.target);
|
|
177
|
+
if (!trigger || isMobile()) return;
|
|
178
|
+
|
|
179
|
+
if (focusedTrigger === trigger) {
|
|
180
|
+
focusedTrigger = null;
|
|
181
|
+
}
|
|
182
|
+
if (hoveredTrigger === trigger) return;
|
|
183
|
+
|
|
184
|
+
hideTooltip(trigger);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
document.addEventListener("click", function(event) {
|
|
188
|
+
var trigger = closestTrigger(event.target);
|
|
189
|
+
|
|
190
|
+
if (trigger) {
|
|
191
|
+
if (!isMobile()) return;
|
|
192
|
+
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
event.stopPropagation();
|
|
195
|
+
toggleTooltip(trigger);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (activeTrigger && isMobile()) {
|
|
200
|
+
hideTooltip();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
document.addEventListener("keydown", function(event) {
|
|
205
|
+
if (event.key !== "Escape") return;
|
|
206
|
+
|
|
207
|
+
hideTooltip();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
window.addEventListener("scroll", positionTooltip, true);
|
|
211
|
+
window.addEventListener("resize", function() {
|
|
212
|
+
hideTooltip();
|
|
213
|
+
});
|
|
214
|
+
})();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
.admin-tooltip-trigger {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
width: 1.25rem;
|
|
6
|
+
min-width: 1.25rem;
|
|
7
|
+
height: 1.25rem;
|
|
8
|
+
margin: 0 0 0 0.25rem;
|
|
9
|
+
padding: 0;
|
|
10
|
+
border: 1px solid #7a8594;
|
|
11
|
+
border-radius: 50%;
|
|
12
|
+
background: #fff;
|
|
13
|
+
color: #4b5563;
|
|
14
|
+
cursor: help;
|
|
15
|
+
font-size: 0.75rem;
|
|
16
|
+
font-weight: 700;
|
|
17
|
+
line-height: 1;
|
|
18
|
+
vertical-align: middle;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.admin-tooltip-trigger:hover,
|
|
22
|
+
.admin-tooltip-trigger:focus-visible,
|
|
23
|
+
.admin-tooltip-trigger[aria-expanded="true"] {
|
|
24
|
+
border-color: #0d6efd;
|
|
25
|
+
background: #f8fbff;
|
|
26
|
+
color: #0d6efd;
|
|
27
|
+
box-shadow: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.admin-tooltip-trigger__icon {
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.admin-tooltip {
|
|
35
|
+
position: fixed;
|
|
36
|
+
z-index: 1100;
|
|
37
|
+
max-width: 280px;
|
|
38
|
+
padding: 8px 10px;
|
|
39
|
+
border-radius: 4px;
|
|
40
|
+
background: #1f2933;
|
|
41
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
|
|
42
|
+
color: #fff;
|
|
43
|
+
font-size: 0.82rem;
|
|
44
|
+
line-height: 1.45;
|
|
45
|
+
opacity: 0;
|
|
46
|
+
overflow-wrap: anywhere;
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
transform: translateY(2px);
|
|
49
|
+
transition: opacity 0.12s ease, transform 0.12s ease;
|
|
50
|
+
white-space: normal;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.admin-tooltip--visible {
|
|
54
|
+
opacity: 1;
|
|
55
|
+
transform: translateY(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.admin-tooltip::before {
|
|
59
|
+
content: "";
|
|
60
|
+
position: absolute;
|
|
61
|
+
left: var(--admin-tooltip-arrow-left, 50%);
|
|
62
|
+
width: 8px;
|
|
63
|
+
height: 8px;
|
|
64
|
+
background: #1f2933;
|
|
65
|
+
transform: rotate(45deg);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.admin-tooltip[data-admin-tooltip-placement="top"]::before {
|
|
69
|
+
bottom: -4px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.admin-tooltip[data-admin-tooltip-placement="bottom"]::before {
|
|
73
|
+
top: -4px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@media screen and (max-width: 767px) {
|
|
77
|
+
.admin-tooltip-trigger {
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.admin-tooltip {
|
|
82
|
+
max-width: calc(100vw - 24px);
|
|
83
|
+
font-size: 0.85rem;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YummyGuide
|
|
4
|
+
module Administrate
|
|
5
|
+
module TooltipHelper
|
|
6
|
+
def admin_tooltip(text = nil, aria_label: "説明を表示", class: nil, data: {}, &block)
|
|
7
|
+
return if text.blank? && !block_given?
|
|
8
|
+
|
|
9
|
+
custom_class = binding.local_variable_get(:class)
|
|
10
|
+
content_id = admin_tooltip_content_id if block_given?
|
|
11
|
+
|
|
12
|
+
safe_join([
|
|
13
|
+
content_tag(
|
|
14
|
+
:button,
|
|
15
|
+
type: "button",
|
|
16
|
+
class: token_list("admin-tooltip-trigger", custom_class),
|
|
17
|
+
data: admin_tooltip_data_attributes(data, text: text.to_s, content_id: content_id),
|
|
18
|
+
aria: {
|
|
19
|
+
label: aria_label,
|
|
20
|
+
expanded: "false"
|
|
21
|
+
}
|
|
22
|
+
) do
|
|
23
|
+
tag.span("?", class: "admin-tooltip-trigger__icon", aria: { hidden: true })
|
|
24
|
+
end,
|
|
25
|
+
(content_tag(:template, capture(&block), id: content_id) if content_id)
|
|
26
|
+
].compact)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def admin_tooltip_content_id
|
|
32
|
+
@admin_tooltip_index ||= 0
|
|
33
|
+
@admin_tooltip_index += 1
|
|
34
|
+
"admin-tooltip-content-#{@admin_tooltip_index}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def admin_tooltip_data_attributes(data, text:, content_id:)
|
|
38
|
+
attributes = (data || {}).to_h.deep_dup
|
|
39
|
+
attributes.delete(:admin_tooltip_trigger)
|
|
40
|
+
attributes.delete("admin_tooltip_trigger")
|
|
41
|
+
attributes.delete(:admin_tooltip_text)
|
|
42
|
+
attributes.delete("admin_tooltip_text")
|
|
43
|
+
attributes.delete(:admin_tooltip_content_id)
|
|
44
|
+
attributes.delete("admin_tooltip_content_id")
|
|
45
|
+
|
|
46
|
+
attributes[:admin_tooltip_trigger] = true
|
|
47
|
+
if content_id
|
|
48
|
+
attributes[:admin_tooltip_content_id] = content_id
|
|
49
|
+
else
|
|
50
|
+
attributes[:admin_tooltip_text] = text
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
attributes
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -15,6 +15,7 @@ module YummyGuide
|
|
|
15
15
|
yummy_guide_administrate/filter_controls.js
|
|
16
16
|
yummy_guide_administrate/filter_form.js
|
|
17
17
|
yummy_guide_administrate/sticky_left_columns.js
|
|
18
|
+
yummy_guide_administrate/tooltips.js
|
|
18
19
|
yummy_guide_administrate/resizable_navigation.js
|
|
19
20
|
yummy_guide_administrate/sticky_table_headers.js
|
|
20
21
|
]
|
|
@@ -27,6 +28,7 @@ module YummyGuide
|
|
|
27
28
|
::Administrate::ApplicationController.helper YummyGuide::Administrate::NumberInputHelper
|
|
28
29
|
::Administrate::ApplicationController.helper YummyGuide::Administrate::FilterFormHelper
|
|
29
30
|
::Administrate::ApplicationController.helper YummyGuide::Administrate::FilterControlsHelper
|
|
31
|
+
::Administrate::ApplicationController.helper YummyGuide::Administrate::TooltipHelper
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
end
|
|
@@ -6,6 +6,7 @@ require "administrate"
|
|
|
6
6
|
require_relative "../../app/helpers/yummy_guide/administrate/number_input_helper"
|
|
7
7
|
require_relative "../../app/helpers/yummy_guide/administrate/filter_form_helper"
|
|
8
8
|
require_relative "../../app/helpers/yummy_guide/administrate/filter_controls_helper"
|
|
9
|
+
require_relative "../../app/helpers/yummy_guide/administrate/tooltip_helper"
|
|
9
10
|
require_relative "administrate/version"
|
|
10
11
|
require_relative "administrate/filters"
|
|
11
12
|
require_relative "administrate/engine"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe "tooltip assets" do
|
|
4
|
+
let(:javascript_source) do
|
|
5
|
+
File.read(File.expand_path("../../../app/assets/javascripts/yummy_guide_administrate/tooltips.js", __dir__))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
let(:stylesheet_source) do
|
|
9
|
+
File.read(File.expand_path("../../../app/assets/stylesheets/yummy_guide_administrate/_tooltips.scss", __dir__))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
let(:engine_source) do
|
|
13
|
+
File.read(File.expand_path("../../../lib/yummy_guide/administrate/engine.rb", __dir__))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# PC では hover と keyboard focus で tooltip を表示するイベント契約を持つことを静的に確認する
|
|
17
|
+
it "supports desktop hover and focus interactions" do
|
|
18
|
+
expect(javascript_source).to include('document.addEventListener("mouseover"')
|
|
19
|
+
expect(javascript_source).to include('document.addEventListener("mouseout"')
|
|
20
|
+
expect(javascript_source).to include('document.addEventListener("focusin"')
|
|
21
|
+
expect(javascript_source).to include('document.addEventListener("focusout"')
|
|
22
|
+
expect(javascript_source).to include("showTooltip(trigger)")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# モバイルではクリックで tooltip 表示をトグルする契約を持つことを静的に確認する
|
|
26
|
+
it "supports mobile click toggling" do
|
|
27
|
+
expect(javascript_source).to include('MOBILE_MEDIA_QUERY = "(max-width: 767px)"')
|
|
28
|
+
expect(javascript_source).to include("window.matchMedia")
|
|
29
|
+
expect(javascript_source).to include('document.addEventListener("click"')
|
|
30
|
+
expect(javascript_source).to include("toggleTooltip(trigger)")
|
|
31
|
+
expect(javascript_source).to include("event.stopPropagation()")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Esc / 外側クリック / resize で表示中 tooltip を閉じられることを静的に確認する
|
|
35
|
+
it "closes the active tooltip from common dismissal events" do
|
|
36
|
+
expect(javascript_source).to include('event.key !== "Escape"')
|
|
37
|
+
expect(javascript_source).to include("if (activeTrigger && isMobile())")
|
|
38
|
+
expect(javascript_source).to include('window.addEventListener("resize"')
|
|
39
|
+
expect(javascript_source).to include("hideTooltip()")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# block本文用のtemplateがある場合はHTML本文としてtooltipへ描画することを静的に確認する
|
|
43
|
+
it "renders template content for block tooltip bodies" do
|
|
44
|
+
expect(javascript_source).to include('getAttribute("data-admin-tooltip-content-id")')
|
|
45
|
+
expect(javascript_source).to include("template.innerHTML")
|
|
46
|
+
expect(javascript_source).to include("tooltipElement.innerHTML = content.html")
|
|
47
|
+
expect(javascript_source).to include("tooltipElement.textContent = content.text")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# tooltip の吹き出しと表示状態に必要な CSS class が定義されることを静的に確認する
|
|
51
|
+
it "defines tooltip trigger and bubble styles" do
|
|
52
|
+
expect(stylesheet_source).to include(".admin-tooltip-trigger")
|
|
53
|
+
expect(stylesheet_source).to include(".admin-tooltip")
|
|
54
|
+
expect(stylesheet_source).to include(".admin-tooltip--visible")
|
|
55
|
+
expect(stylesheet_source).to include('data-admin-tooltip-placement="top"')
|
|
56
|
+
expect(stylesheet_source).to include("max-width: calc(100vw - 24px)")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# tooltip の JS asset が engine の precompile 対象に入ることを確認する
|
|
60
|
+
it "precompiles the tooltip javascript asset" do
|
|
61
|
+
expect(engine_source).to include("yummy_guide_administrate/tooltips.js")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
|
|
6
|
+
RSpec.describe YummyGuide::Administrate::TooltipHelper do
|
|
7
|
+
subject(:helper_host) do
|
|
8
|
+
Class.new do
|
|
9
|
+
include ActionView::Context
|
|
10
|
+
include ActionView::Helpers::CaptureHelper
|
|
11
|
+
include ActionView::Helpers::OutputSafetyHelper
|
|
12
|
+
include ActionView::Helpers::TagHelper
|
|
13
|
+
include YummyGuide::Administrate::TooltipHelper
|
|
14
|
+
end.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fragment(html)
|
|
18
|
+
Nokogiri::HTML.fragment(html)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe "#admin_tooltip" do
|
|
22
|
+
# tooltip 表示用の button と JS が参照する data 属性を描画することを確認する
|
|
23
|
+
it "renders an icon button tooltip trigger" do
|
|
24
|
+
document = fragment(helper_host.admin_tooltip("公開ページに表示されます。"))
|
|
25
|
+
button = document.at_css("button.admin-tooltip-trigger")
|
|
26
|
+
|
|
27
|
+
expect(button["type"]).to eq("button")
|
|
28
|
+
expect(button["data-admin-tooltip-trigger"]).to eq("true")
|
|
29
|
+
expect(button["data-admin-tooltip-text"]).to eq("公開ページに表示されます。")
|
|
30
|
+
expect(button["aria-label"]).to eq("説明を表示")
|
|
31
|
+
expect(button["aria-expanded"]).to eq("false")
|
|
32
|
+
expect(button.at_css(".admin-tooltip-trigger__icon").text).to eq("?")
|
|
33
|
+
expect(button.at_css(".admin-tooltip-trigger__icon")["aria-hidden"]).to eq("true")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# tooltip 本文に HTML が含まれても子要素として展開されず data 属性へ escape されることを確認する
|
|
37
|
+
it "escapes tooltip text instead of rendering it as HTML" do
|
|
38
|
+
html = helper_host.admin_tooltip(%(<strong onclick="alert(1)">説明</strong>))
|
|
39
|
+
document = fragment(html)
|
|
40
|
+
button = document.at_css("button.admin-tooltip-trigger")
|
|
41
|
+
|
|
42
|
+
expect(document.at_css("strong")).to be_nil
|
|
43
|
+
expect(button["data-admin-tooltip-text"]).to eq(%(<strong onclick="alert(1)">説明</strong>))
|
|
44
|
+
expect(html).to include("<strong")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# block を渡した場合は説明本文として template に描画し、text 引数より優先されることを確認する
|
|
48
|
+
it "renders block content as the tooltip body before text content" do
|
|
49
|
+
document = fragment(
|
|
50
|
+
helper_host.admin_tooltip("Fallback text") do
|
|
51
|
+
helper_host.safe_join([
|
|
52
|
+
helper_host.content_tag(:span, "Add - 調整レコードを追加表示するようにします。"),
|
|
53
|
+
helper_host.tag.br,
|
|
54
|
+
helper_host.content_tag(:span, "Only - 調整レコードのみを表示します。")
|
|
55
|
+
])
|
|
56
|
+
end
|
|
57
|
+
)
|
|
58
|
+
button = document.at_css("button.admin-tooltip-trigger")
|
|
59
|
+
template = document.at_css("template")
|
|
60
|
+
|
|
61
|
+
expect(button["data-admin-tooltip-text"]).to be_nil
|
|
62
|
+
expect(button["data-admin-tooltip-content-id"]).to eq(template["id"])
|
|
63
|
+
expect(template.text).to include("Add - 調整レコードを追加表示するようにします。")
|
|
64
|
+
expect(template.text).to include("Only - 調整レコードのみを表示します。")
|
|
65
|
+
expect(template.at_css("br")).to be_present
|
|
66
|
+
expect(template.text).not_to include("Fallback text")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 呼び出し側が指定した class / data / aria label を維持しつつ tooltip 用 data を優先することを確認する
|
|
70
|
+
it "merges custom attributes without allowing tooltip data overrides" do
|
|
71
|
+
document = fragment(
|
|
72
|
+
helper_host.admin_tooltip(
|
|
73
|
+
"ステータスの補足です。",
|
|
74
|
+
aria_label: "補足を開く",
|
|
75
|
+
class: "status-tooltip",
|
|
76
|
+
data: {
|
|
77
|
+
tracking_key: "status",
|
|
78
|
+
admin_tooltip_text: "上書きされない説明"
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
button = document.at_css("button.admin-tooltip-trigger")
|
|
83
|
+
|
|
84
|
+
expect(button["class"]).to include("admin-tooltip-trigger", "status-tooltip")
|
|
85
|
+
expect(button["aria-label"]).to eq("補足を開く")
|
|
86
|
+
expect(button["data-tracking-key"]).to eq("status")
|
|
87
|
+
expect(button["data-admin-tooltip-trigger"]).to eq("true")
|
|
88
|
+
expect(button["data-admin-tooltip-text"]).to eq("ステータスの補足です。")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 空の説明文では空ボタンを描画しないことを確認する
|
|
92
|
+
it "does not render a trigger for blank text" do
|
|
93
|
+
expect(helper_host.admin_tooltip("")).to be_nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yummy-guide-generic-administrate
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- akatsuki-kk
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: administrate
|
|
@@ -99,10 +99,12 @@ files:
|
|
|
99
99
|
- app/assets/javascripts/yummy_guide_administrate/resizable_navigation.js
|
|
100
100
|
- app/assets/javascripts/yummy_guide_administrate/sticky_left_columns.js
|
|
101
101
|
- app/assets/javascripts/yummy_guide_administrate/sticky_table_headers.js
|
|
102
|
+
- app/assets/javascripts/yummy_guide_administrate/tooltips.js
|
|
102
103
|
- app/assets/stylesheets/yummy_guide_administrate/_column_resizer.scss
|
|
103
104
|
- app/assets/stylesheets/yummy_guide_administrate/_datetime_input.scss
|
|
104
105
|
- app/assets/stylesheets/yummy_guide_administrate/_fixed_submit_actions.scss
|
|
105
106
|
- app/assets/stylesheets/yummy_guide_administrate/_resizable_navigation.scss
|
|
107
|
+
- app/assets/stylesheets/yummy_guide_administrate/_tooltips.scss
|
|
106
108
|
- app/assets/stylesheets/yummy_guide_administrate/components.scss
|
|
107
109
|
- app/controllers/concerns/yummy_guide/administrate/datetime_filter_parameters.rb
|
|
108
110
|
- app/controllers/concerns/yummy_guide/administrate/default_sorting.rb
|
|
@@ -116,6 +118,7 @@ files:
|
|
|
116
118
|
- app/helpers/yummy_guide/administrate/filter_controls_helper.rb
|
|
117
119
|
- app/helpers/yummy_guide/administrate/filter_form_helper.rb
|
|
118
120
|
- app/helpers/yummy_guide/administrate/number_input_helper.rb
|
|
121
|
+
- app/helpers/yummy_guide/administrate/tooltip_helper.rb
|
|
119
122
|
- app/views/fields/number/_form.html.erb
|
|
120
123
|
- app/views/fields/yummy_guide_administrate/area/picture/_form.html.erb
|
|
121
124
|
- app/views/fields/yummy_guide_administrate/area/picture/_index.html.erb
|
|
@@ -152,6 +155,8 @@ files:
|
|
|
152
155
|
- spec/yummy_guide/administrate/filter_form_helper_spec.rb
|
|
153
156
|
- spec/yummy_guide/administrate/filters_spec.rb
|
|
154
157
|
- spec/yummy_guide/administrate/number_input_helper_spec.rb
|
|
158
|
+
- spec/yummy_guide/administrate/tooltip_asset_spec.rb
|
|
159
|
+
- spec/yummy_guide/administrate/tooltip_helper_spec.rb
|
|
155
160
|
- yummy-guide-generic-administrate.gemspec
|
|
156
161
|
homepage: https://github.com/yummy-guide/generic-administrate
|
|
157
162
|
licenses:
|