yummy-guide-generic-administrate 0.8.18 → 0.8.19

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b53bff1d61f85a0cf761db5b1268073c026170320b44cd8b02f1f8ebdcf42938
4
- data.tar.gz: e1e7da598410b265d230ba48a204e604539c9e33e4318027ba0f03152e7083f9
3
+ metadata.gz: c85a364e43e99ea2fcb9440143675804196ced1b1d46b211cd202e1664232581
4
+ data.tar.gz: e6974ae9c103179acfc9fe77347336dac3feae4e305f1008dbc2ebd18eb1bd26
5
5
  SHA512:
6
- metadata.gz: 973806241c05cb384995d1ce293af242adda092846ff9c0d94790bed8c4e04f82e7d418dbce3b1fa29843ab56d6e3c022c72d695da8f7fe3aff95806c7615f7c
7
- data.tar.gz: b02c11686425c3ca4c120e7d286a076595ec3074f640252918d8278b5bbaa4d6dd58167482c23afe400dcc54617c905103c988cbf23876ebd3f08b56d4f5e2e7
6
+ metadata.gz: bddc130440fc835aabeca11cc79dafcaaecfcd6f06ab4c37a18318b618a4d20b835436e5f766adb4f31a4e25f391a447dbdce40af0e2694b2101f426f2153109
7
+ data.tar.gz: 87927272596c5bbb430f78982a5ba8d4e20626c88b907fa14613708a49de8d4dc357d8fe4c8160719f00a8e3d159aaea74dee74678aa1c22f5c41430fbadb117
@@ -6,10 +6,13 @@
6
6
  var MOBILE_MEDIA_QUERY = "(max-width: 767px)";
7
7
  var EDGE_GAP = 8;
8
8
  var OFFSET = 8;
9
+ var WARNING_HOVER_HIDE_DELAY = 300;
9
10
  var activeTrigger = null;
10
11
  var hoveredTrigger = null;
11
12
  var focusedTrigger = null;
13
+ var pinnedTrigger = null;
12
14
  var tooltipElement = null;
15
+ var warningHideTimer = null;
13
16
 
14
17
  function isMobile() {
15
18
  return window.matchMedia && window.matchMedia(MOBILE_MEDIA_QUERY).matches;
@@ -21,6 +24,39 @@
21
24
  return target.closest(TRIGGER_SELECTOR);
22
25
  }
23
26
 
27
+ function warningTrigger(trigger) {
28
+ return trigger && trigger.getAttribute("data-admin-tooltip-variant") === "warning";
29
+ }
30
+
31
+ function interactiveTooltipTarget(target) {
32
+ return tooltipElement &&
33
+ tooltipElement.classList.contains("admin-tooltip--warning") &&
34
+ target &&
35
+ tooltipElement.contains(target);
36
+ }
37
+
38
+ function clearWarningHideTimer() {
39
+ if (!warningHideTimer) return;
40
+
41
+ window.clearTimeout(warningHideTimer);
42
+ warningHideTimer = null;
43
+ }
44
+
45
+ function hideTooltipAfterPointerLeave(trigger) {
46
+ if (!warningTrigger(trigger)) {
47
+ hideTooltip(trigger);
48
+ return;
49
+ }
50
+
51
+ clearWarningHideTimer();
52
+ warningHideTimer = window.setTimeout(function() {
53
+ warningHideTimer = null;
54
+ if (activeTrigger !== trigger || focusedTrigger === trigger || pinnedTrigger === trigger) return;
55
+
56
+ hideTooltip(trigger);
57
+ }, WARNING_HOVER_HIDE_DELAY);
58
+ }
59
+
24
60
  function clamp(value, min, max) {
25
61
  return Math.min(Math.max(value, min), max);
26
62
  }
@@ -37,12 +73,23 @@
37
73
  return tooltipElement;
38
74
  }
39
75
 
76
+ function templateContentHtml(template) {
77
+ var html = template.innerHTML;
78
+ if (html) return html;
79
+
80
+ // React は <template> の子要素を template.content ではなく直接配置するため、
81
+ // innerHTML が空の場合は直接の子ノードから本文を復元する。
82
+ return Array.prototype.map.call(template.childNodes, function(node) {
83
+ return node.outerHTML || node.textContent || "";
84
+ }).join("");
85
+ }
86
+
40
87
  function tooltipContent(trigger) {
41
88
  var contentId = trigger.getAttribute("data-admin-tooltip-content-id");
42
89
  var template = contentId ? document.getElementById(contentId) : null;
43
90
  if (template) {
44
91
  return {
45
- html: template.innerHTML,
92
+ html: templateContentHtml(template),
46
93
  text: ""
47
94
  };
48
95
  }
@@ -59,7 +106,14 @@
59
106
 
60
107
  trigger.setAttribute("aria-expanded", expanded ? "true" : "false");
61
108
 
62
- if (expanded) {
109
+ if (warningTrigger(trigger)) {
110
+ trigger.removeAttribute("aria-describedby");
111
+ if (expanded) {
112
+ trigger.setAttribute("aria-controls", TOOLTIP_ID);
113
+ } else {
114
+ trigger.removeAttribute("aria-controls");
115
+ }
116
+ } else if (expanded) {
63
117
  trigger.setAttribute("aria-describedby", TOOLTIP_ID);
64
118
  } else {
65
119
  trigger.removeAttribute("aria-describedby");
@@ -106,8 +160,19 @@
106
160
  hideTooltip();
107
161
  }
108
162
 
163
+ clearWarningHideTimer();
109
164
  activeTrigger = trigger;
110
165
  tooltipElement = getTooltipElement();
166
+ tooltipElement.classList.toggle(
167
+ "admin-tooltip--warning",
168
+ warningTrigger(trigger)
169
+ );
170
+ tooltipElement.setAttribute("role", warningTrigger(trigger) ? "dialog" : "tooltip");
171
+ if (warningTrigger(trigger)) {
172
+ tooltipElement.setAttribute("aria-label", trigger.getAttribute("aria-label") || "Details");
173
+ } else {
174
+ tooltipElement.removeAttribute("aria-label");
175
+ }
111
176
  if (content.html) {
112
177
  tooltipElement.innerHTML = content.html;
113
178
  } else {
@@ -126,6 +191,7 @@
126
191
  function hideTooltip(trigger) {
127
192
  if (trigger && activeTrigger !== trigger) return;
128
193
 
194
+ clearWarningHideTimer();
129
195
  setTriggerState(activeTrigger, false);
130
196
 
131
197
  if (tooltipElement) {
@@ -133,14 +199,16 @@
133
199
  }
134
200
 
135
201
  activeTrigger = null;
202
+ pinnedTrigger = null;
136
203
  }
137
204
 
138
205
  function toggleTooltip(trigger) {
139
- if (activeTrigger === trigger && tooltipElement && tooltipElement.classList.contains(VISIBLE_CLASS)) {
206
+ if (pinnedTrigger === trigger && tooltipElement && tooltipElement.classList.contains(VISIBLE_CLASS)) {
140
207
  hideTooltip(trigger);
141
208
  return;
142
209
  }
143
210
 
211
+ pinnedTrigger = trigger;
144
212
  showTooltip(trigger);
145
213
  }
146
214
 
@@ -148,23 +216,47 @@
148
216
  var trigger = closestTrigger(event.target);
149
217
  if (!trigger || isMobile() || (event.relatedTarget && trigger.contains(event.relatedTarget))) return;
150
218
 
219
+ clearWarningHideTimer();
151
220
  hoveredTrigger = trigger;
152
221
  showTooltip(trigger);
153
222
  });
154
223
 
224
+ document.addEventListener("mouseover", function(event) {
225
+ if (interactiveTooltipTarget(event.target)) {
226
+ clearWarningHideTimer();
227
+ }
228
+ });
229
+
155
230
  document.addEventListener("mouseout", function(event) {
156
231
  var trigger = closestTrigger(event.target);
157
- if (!trigger || isMobile() || (event.relatedTarget && trigger.contains(event.relatedTarget))) return;
232
+ if (!trigger || isMobile() || (event.relatedTarget && (trigger.contains(event.relatedTarget) || interactiveTooltipTarget(event.relatedTarget)))) return;
158
233
 
159
234
  if (hoveredTrigger === trigger) {
160
235
  hoveredTrigger = null;
161
236
  }
162
- if (focusedTrigger === trigger) return;
237
+ if (focusedTrigger === trigger || pinnedTrigger === trigger) return;
163
238
 
164
- hideTooltip(trigger);
239
+ hideTooltipAfterPointerLeave(trigger);
240
+ });
241
+
242
+ document.addEventListener("mouseout", function(event) {
243
+ if (!interactiveTooltipTarget(event.target) || interactiveTooltipTarget(event.relatedTarget)) return;
244
+ if (activeTrigger && event.relatedTarget && activeTrigger.contains(event.relatedTarget)) return;
245
+
246
+ if (hoveredTrigger === activeTrigger) {
247
+ hoveredTrigger = null;
248
+ }
249
+ if (focusedTrigger === activeTrigger || pinnedTrigger === activeTrigger) return;
250
+
251
+ hideTooltipAfterPointerLeave(activeTrigger);
165
252
  });
166
253
 
167
254
  document.addEventListener("focusin", function(event) {
255
+ if (interactiveTooltipTarget(event.target)) {
256
+ clearWarningHideTimer();
257
+ return;
258
+ }
259
+
168
260
  var trigger = closestTrigger(event.target);
169
261
  if (!trigger || isMobile()) return;
170
262
 
@@ -179,24 +271,36 @@
179
271
  if (focusedTrigger === trigger) {
180
272
  focusedTrigger = null;
181
273
  }
182
- if (hoveredTrigger === trigger) return;
274
+ if (warningTrigger(trigger) && interactiveTooltipTarget(event.relatedTarget)) {
275
+ clearWarningHideTimer();
276
+ return;
277
+ }
278
+ if (hoveredTrigger === trigger || pinnedTrigger === trigger) return;
183
279
 
184
280
  hideTooltip(trigger);
185
281
  });
186
282
 
283
+ document.addEventListener("focusout", function(event) {
284
+ if (!interactiveTooltipTarget(event.target)) return;
285
+ if (interactiveTooltipTarget(event.relatedTarget)) return;
286
+ if (activeTrigger && event.relatedTarget && (activeTrigger === event.relatedTarget || activeTrigger.contains(event.relatedTarget))) return;
287
+
288
+ hideTooltip();
289
+ });
290
+
187
291
  document.addEventListener("click", function(event) {
188
292
  var trigger = closestTrigger(event.target);
189
293
 
190
294
  if (trigger) {
191
- if (!isMobile()) return;
192
-
193
295
  event.preventDefault();
194
296
  event.stopPropagation();
195
297
  toggleTooltip(trigger);
196
298
  return;
197
299
  }
198
300
 
199
- if (activeTrigger && isMobile()) {
301
+ if (interactiveTooltipTarget(event.target)) return;
302
+
303
+ if (activeTrigger) {
200
304
  hideTooltip();
201
305
  }
202
306
  });
@@ -55,6 +55,73 @@
55
55
  transform: translateY(0);
56
56
  }
57
57
 
58
+ .admin-tooltip--warning {
59
+ max-width: 360px;
60
+ padding: 0;
61
+ border: 1px solid #fedf89;
62
+ border-left: 4px solid #f79009;
63
+ background: #fff;
64
+ color: #344054;
65
+ }
66
+
67
+ .admin-tooltip--warning.admin-tooltip--visible {
68
+ pointer-events: auto;
69
+ }
70
+
71
+ .admin-tooltip--warning::before {
72
+ background: #fff;
73
+ }
74
+
75
+ .admin-tooltip__warning-card {
76
+ padding: 0.75rem;
77
+ }
78
+
79
+ .admin-tooltip__warning-title {
80
+ display: block;
81
+ color: #7a2e0e;
82
+ }
83
+
84
+ .admin-tooltip__warning-message {
85
+ margin: 0.35rem 0 0.75rem;
86
+ }
87
+
88
+ .admin-tooltip__warning-details {
89
+ display: grid;
90
+ gap: 0.5rem;
91
+ margin: 0;
92
+ }
93
+
94
+ .admin-tooltip__warning-details > div {
95
+ display: grid;
96
+ grid-template-columns: minmax(0, 1fr) auto;
97
+ align-items: baseline;
98
+ gap: 0.75rem;
99
+ padding-top: 0.5rem;
100
+ border-top: 1px solid #eaecf0;
101
+ }
102
+
103
+ .admin-tooltip__warning-details dt {
104
+ color: #667085;
105
+ font-weight: 600;
106
+ }
107
+
108
+ .admin-tooltip__warning-details dd {
109
+ margin: 0;
110
+ color: #344054;
111
+ text-align: right;
112
+ }
113
+
114
+ .admin-tooltip__warning-details dt,
115
+ .admin-tooltip__warning-details dd {
116
+ line-height: 1.45;
117
+ }
118
+
119
+ .admin-tooltip__warning-actions {
120
+ display: flex;
121
+ justify-content: flex-end;
122
+ margin-top: 0.75rem;
123
+ }
124
+
58
125
  .admin-tooltip::before {
59
126
  content: "";
60
127
  position: absolute;
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YummyGuide
4
+ module Administrate
5
+ module IntegrationTransitionHelper
6
+ # 外部連携の非同期遷移を、製品固有の文言を渡して共通表示する。
7
+ def integration_transition_notice(message:, state:, details: nil)
8
+ warning = state.to_s == 'warning'
9
+ color = (warning || state.to_s == 'failed') ? '#b42318' : '#175cd3'
10
+ background = warning ? '#fef3f2' : '#f8fafc'
11
+ border = warning ? '#fecdca' : color
12
+ icon = if warning
13
+ content_tag(
14
+ :span,
15
+ '⚠',
16
+ class: 'integration-transition-notice__caution-icon',
17
+ aria: { hidden: true },
18
+ style: "color: #{color}; font-size: 20px; line-height: 1;",
19
+ )
20
+ end
21
+
22
+ content_tag(
23
+ :div,
24
+ role: 'status',
25
+ class: "integration-transition-notice integration-transition-notice--#{state}",
26
+ data: { integration_transition_state: state },
27
+ style: "display: flex; gap: 8px; margin: 12px 0; padding: 12px; border: 1px solid #{border}; border-left: 4px solid #{color}; background: #{background}; color: #{color};",
28
+ ) do
29
+ safe_join([
30
+ icon,
31
+ content_tag(:div, safe_join([content_tag(:strong, message), details.present? ? content_tag(:div, details, style: 'margin-top: 4px;') : nil].compact)),
32
+ ].compact)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -27,6 +27,7 @@ module YummyGuide
27
27
  ::Administrate::ApplicationController.helper YummyGuide::Administrate::FilterFormHelper
28
28
  ::Administrate::ApplicationController.helper YummyGuide::Administrate::FilterControlsHelper
29
29
  ::Administrate::ApplicationController.helper YummyGuide::Administrate::TooltipHelper
30
+ ::Administrate::ApplicationController.helper YummyGuide::Administrate::IntegrationTransitionHelper
30
31
  end
31
32
  end
32
33
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module YummyGuide
4
4
  module Administrate
5
- VERSION = "0.8.18"
5
+ VERSION = "0.8.19"
6
6
  end
7
7
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe YummyGuide::Administrate::IntegrationTransitionHelper do
6
+ let(:view) { ActionView::Base.empty }
7
+
8
+ before { view.extend(described_class) }
9
+
10
+ # 製品固有の非同期連携状態を共通のアクセシブルな通知として表示すること
11
+ it "renders a status notice with the supplied message and details" do
12
+ html = view.integration_transition_notice(message: "処理中です", state: "processing", details: "対象 Assign: 2件")
13
+
14
+ expect(html).to include('role="status"')
15
+ expect(html).to include("処理中です")
16
+ expect(html).to include("対象 Assign: 2件")
17
+ end
18
+
19
+ # 紐付け設定が必要な警告は赤色カードとcautionアイコンで強調すること
20
+ it "renders a warning notice as a caution card" do
21
+ html = view.integration_transition_notice(message: "JobOption の紐付け設定が必要", state: "warning")
22
+
23
+ expect(html).to include("integration-transition-notice--warning")
24
+ expect(html).to include('data-integration-transition-state="warning"')
25
+ expect(html).to include("integration-transition-notice__caution-icon")
26
+ expect(html).to include("#fef3f2")
27
+ end
28
+ end
@@ -22,31 +22,76 @@ RSpec.describe "tooltip assets" do
22
22
  expect(javascript_source).to include("showTooltip(trigger)")
23
23
  end
24
24
 
25
- # モバイルではクリックで tooltip 表示をトグルする契約を持つことを静的に確認する
26
- it "supports mobile click toggling" do
25
+ # 画面幅にかかわらずクリックで tooltip 表示を固定・トグルできることを静的に確認する
26
+ it "supports click toggling on every screen size" do
27
27
  expect(javascript_source).to include('MOBILE_MEDIA_QUERY = "(max-width: 767px)"')
28
28
  expect(javascript_source).to include("window.matchMedia")
29
29
  expect(javascript_source).to include('document.addEventListener("click"')
30
30
  expect(javascript_source).to include("toggleTooltip(trigger)")
31
+ expect(javascript_source).to include("pinnedTrigger = trigger")
31
32
  expect(javascript_source).to include("event.stopPropagation()")
32
33
  end
33
34
 
34
35
  # Esc / 外側クリック / resize で表示中 tooltip を閉じられることを静的に確認する
35
36
  it "closes the active tooltip from common dismissal events" do
36
37
  expect(javascript_source).to include('event.key !== "Escape"')
37
- expect(javascript_source).to include("if (activeTrigger && isMobile())")
38
+ expect(javascript_source).to include("if (activeTrigger)")
38
39
  expect(javascript_source).to include('window.addEventListener("resize"')
39
40
  expect(javascript_source).to include("hideTooltip()")
40
41
  end
41
42
 
42
- # block本文用のtemplateがある場合はHTML本文としてtooltipへ描画することを静的に確認する
43
+ # ERBのtemplate本文とReactが直接配置する子要素の両方をHTML本文としてtooltipへ描画することを静的に確認する
43
44
  it "renders template content for block tooltip bodies" do
44
45
  expect(javascript_source).to include('getAttribute("data-admin-tooltip-content-id")')
45
46
  expect(javascript_source).to include("template.innerHTML")
47
+ expect(javascript_source).to include("templateContentHtml")
48
+ expect(javascript_source).to include("Array.prototype.map.call(template.childNodes")
49
+ expect(javascript_source).to include("node.outerHTML || node.textContent")
46
50
  expect(javascript_source).to include("tooltipElement.innerHTML = content.html")
47
51
  expect(javascript_source).to include("tooltipElement.textContent = content.text")
48
52
  end
49
53
 
54
+ # warning variantでは白背景の項目別ポップオーバーを表示できることを静的に確認する
55
+ it "supports warning card tooltip variants" do
56
+ expect(javascript_source).to include('data-admin-tooltip-variant')
57
+ expect(javascript_source).to include('"admin-tooltip--warning"')
58
+ expect(stylesheet_source).to include('.admin-tooltip--warning')
59
+ expect(stylesheet_source).to include('background: #fff')
60
+ expect(stylesheet_source).to include('.admin-tooltip__warning-details')
61
+ expect(stylesheet_source).to include('align-items: baseline')
62
+ expect(stylesheet_source).to match(/\.admin-tooltip--warning\.admin-tooltip--visible\s*\{[^}]*pointer-events: auto;/m)
63
+ end
64
+
65
+ # warning cardはポインターをカード上へ移動しても閉じず、操作要素をクリックできることを静的に確認する
66
+ it "keeps interactive warning cards open while they are hovered" do
67
+ expect(javascript_source).to include('interactiveTooltipTarget(event.relatedTarget)')
68
+ expect(javascript_source).to include('if (interactiveTooltipTarget(event.target)) return')
69
+ expect(javascript_source).to include('WARNING_HOVER_HIDE_DELAY = 300')
70
+ expect(javascript_source).to include('clearWarningHideTimer()')
71
+ expect(javascript_source).to include('hideTooltipAfterPointerLeave(trigger)')
72
+ expect(javascript_source).to include('"dialog"')
73
+ end
74
+
75
+ # warning cardを閉じた後は透明な要素が背後の操作を妨げないことを静的に確認する
76
+ it "enables pointer events only while warning cards are visible" do
77
+ expect(stylesheet_source).to include('.admin-tooltip--warning.admin-tooltip--visible')
78
+ expect(stylesheet_source).to include('pointer-events: none')
79
+ end
80
+
81
+ # triggerとwarning card間のキーボードフォーカス移動では閉じず、外部への移動で閉じることを静的に確認する
82
+ it "keeps warning cards open while focus moves within the interactive card" do
83
+ expect(javascript_source).to include('warningTrigger(trigger) && interactiveTooltipTarget(event.relatedTarget)')
84
+ expect(javascript_source).to include('activeTrigger === event.relatedTarget || activeTrigger.contains(event.relatedTarget)')
85
+ expect(javascript_source).to include('if (!interactiveTooltipTarget(event.target)) return;')
86
+ expect(javascript_source).to include('hideTooltip();')
87
+ end
88
+
89
+ # warning cardの確認操作を詳細項目の右下へ配置するスタイルを持つことを確認する
90
+ it "right-aligns warning card actions" do
91
+ expect(stylesheet_source).to include('.admin-tooltip__warning-actions')
92
+ expect(stylesheet_source).to include('justify-content: flex-end')
93
+ end
94
+
50
95
  # tooltip の吹き出しと表示状態に必要な CSS class が定義されることを静的に確認する
51
96
  it "defines tooltip trigger and bubble styles" do
52
97
  expect(stylesheet_source).to include(".admin-tooltip-trigger")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yummy-guide-generic-administrate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.18
4
+ version: 0.8.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - akatsuki-kk
@@ -118,6 +118,7 @@ files:
118
118
  - app/helpers/yummy_guide/administrate/datetime_input_helper.rb
119
119
  - app/helpers/yummy_guide/administrate/filter_controls_helper.rb
120
120
  - app/helpers/yummy_guide/administrate/filter_form_helper.rb
121
+ - app/helpers/yummy_guide/administrate/integration_transition_helper.rb
121
122
  - app/helpers/yummy_guide/administrate/number_input_helper.rb
122
123
  - app/helpers/yummy_guide/administrate/tooltip_helper.rb
123
124
  - app/views/administrate/application/_header_pagination.html.erb
@@ -161,6 +162,7 @@ files:
161
162
  - spec/yummy_guide/administrate/filter_form_asset_spec.rb
162
163
  - spec/yummy_guide/administrate/filter_form_helper_spec.rb
163
164
  - spec/yummy_guide/administrate/filters_spec.rb
165
+ - spec/yummy_guide/administrate/integration_transition_helper_spec.rb
164
166
  - spec/yummy_guide/administrate/number_input_helper_spec.rb
165
167
  - spec/yummy_guide/administrate/tooltip_asset_spec.rb
166
168
  - spec/yummy_guide/administrate/tooltip_helper_spec.rb