playbook_ui 16.3.0.pre.rc.3 → 16.3.0.pre.rc.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/app/pb_kits/playbook/pb_button/_button.scss +3 -2
- data/app/pb_kits/playbook/pb_icon/icon.rb +168 -19
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_react_reset_key.jsx +100 -0
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_react_reset_key.md +1 -0
- data/app/pb_kits/playbook/pb_multi_level_select/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_multi_level_select/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_pagination/_pagination.scss +101 -1
- data/app/pb_kits/playbook/pb_pagination/_pagination.test.jsx +172 -1
- data/app/pb_kits/playbook/pb_pagination/_pagination.tsx +178 -15
- data/app/pb_kits/playbook/pb_pagination/docs/_pagination_default.jsx +1 -1
- data/app/pb_kits/playbook/pb_textarea/_textarea.tsx +39 -20
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_input_options.jsx +68 -0
- data/app/pb_kits/playbook/pb_textarea/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_textarea/docs/index.js +9 -8
- data/app/pb_kits/playbook/pb_textarea/textarea.html.erb +2 -2
- data/app/pb_kits/playbook/pb_textarea/textarea.rb +6 -2
- data/app/pb_kits/playbook/pb_textarea/textarea.test.js +133 -0
- data/dist/chunks/vendor.js +2 -2
- data/dist/playbook.css +1 -1
- data/lib/playbook/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ed5ebc4bcaa2e9cec8f996f7709586bb131dd974b115837eecc0920b4a7cd0d
|
|
4
|
+
data.tar.gz: 558eacf65043d2b076fd8fc8c2783ec4e70573228f46bc7c68b4ff1f60dba608
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e0436ed47dbf37f88c8f4b98fa939b9c8835ac00124ee73eb8c05d539756eedbe7f95e8fbee85687c2f798221c5b86715afa83722d153ca82614760df715a6fd
|
|
7
|
+
data.tar.gz: aa23810833c0a3bbc70275084d7cb41a342812d31a97af269907f28ac0563afb825dc4c578c498e46e0af1112b3426ddb6a46bf1b74783c213dc70ff6dd89286
|
|
@@ -115,9 +115,10 @@ $pb_button_sizes: (
|
|
|
115
115
|
|
|
116
116
|
// Icon-only button (icon prop set, no text) - square with equal padding
|
|
117
117
|
// Rails: uses .pb_button_icon_only class
|
|
118
|
-
// React:
|
|
118
|
+
// React: when pb_button_content is empty (no text). Do not match when content has
|
|
119
|
+
// text + icon (e.g. "Exit Fullscreen" + FA icon) which can include empty spans.
|
|
119
120
|
&.pb_button_icon_only,
|
|
120
|
-
&:has(.pb_button_content
|
|
121
|
+
&:has(.pb_button_content:empty) {
|
|
121
122
|
aspect-ratio: 1;
|
|
122
123
|
min-width: auto;
|
|
123
124
|
width: auto;
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require "open-uri"
|
|
4
4
|
require "json"
|
|
5
|
+
require "digest"
|
|
5
6
|
|
|
6
7
|
module Playbook
|
|
7
8
|
module PbIcon
|
|
8
9
|
class Icon < Playbook::KitBase
|
|
10
|
+
ICON_PATH_DEV_CACHE_TTL_SECONDS = 2
|
|
11
|
+
ICON_PATH_PROD_CACHE_TTL_SECONDS = 60
|
|
12
|
+
|
|
9
13
|
prop :border, type: Playbook::Props::Boolean,
|
|
10
14
|
default: false
|
|
11
15
|
prop :fixed_width, type: Playbook::Props::Boolean,
|
|
@@ -82,30 +86,35 @@ module Playbook
|
|
|
82
86
|
)
|
|
83
87
|
end
|
|
84
88
|
|
|
89
|
+
# Instance-level memoization of alias map lookup result
|
|
85
90
|
def icon_alias_map
|
|
86
|
-
return
|
|
91
|
+
return @icon_alias_map if defined?(@icon_alias_map)
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
json = File.read(Rails.root.join(base_path))
|
|
90
|
-
JSON.parse(json)["aliases"].freeze
|
|
93
|
+
@icon_alias_map = self.class.icon_alias_map
|
|
91
94
|
end
|
|
92
95
|
|
|
96
|
+
# Instance-level memoization of resolved asset path
|
|
93
97
|
def asset_path
|
|
94
|
-
return
|
|
98
|
+
return @asset_path if defined?(@asset_path)
|
|
99
|
+
|
|
100
|
+
@asset_path =
|
|
101
|
+
if Rails.application.config.respond_to?(:icon_path)
|
|
102
|
+
resolved_icon = resolve_alias(icon)
|
|
103
|
+
path = self.class.icon_path_index[resolved_icon]
|
|
104
|
+
path if path && File.exist?(path)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def is_svg?
|
|
109
|
+
return @is_svg if defined?(@is_svg)
|
|
95
110
|
|
|
96
|
-
|
|
97
|
-
resolved_icon = resolve_alias(icon)
|
|
98
|
-
icon_path = Dir.glob(Rails.root.join(base_path, "**", "#{resolved_icon}.svg")).first
|
|
99
|
-
icon_path if icon_path && File.exist?(icon_path)
|
|
111
|
+
@is_svg = (icon || custom_icon.to_s).include?(".svg") || asset_path.present?
|
|
100
112
|
end
|
|
101
113
|
|
|
102
114
|
def render_svg
|
|
103
115
|
doc = Nokogiri::XML(URI.open(asset_path || icon || custom_icon)) # rubocop:disable Security/Open
|
|
104
|
-
svg = doc.at_css
|
|
105
|
-
|
|
106
|
-
unless svg
|
|
107
|
-
return "" # Return an empty string if SVG element is not found
|
|
108
|
-
end
|
|
116
|
+
svg = doc.at_css("svg")
|
|
117
|
+
return "" unless svg
|
|
109
118
|
|
|
110
119
|
svg["class"] = %w[pb_custom_icon svg-inline--fa].concat([object.custom_icon_classname]).join(" ")
|
|
111
120
|
svg["id"] = object.id
|
|
@@ -113,7 +122,9 @@ module Playbook
|
|
|
113
122
|
svg["width"] = "auto"
|
|
114
123
|
svg["tabindex"] = object.tabindex
|
|
115
124
|
fill_color = object.color || "currentColor"
|
|
116
|
-
|
|
125
|
+
|
|
126
|
+
# Safely apply fill to all paths (avoids nil errors + handles multi-path icons)
|
|
127
|
+
doc.css("path").each { |p| p["fill"] = fill_color }
|
|
117
128
|
|
|
118
129
|
if object.data.present?
|
|
119
130
|
object.data.each do |key, value|
|
|
@@ -135,14 +146,152 @@ module Playbook
|
|
|
135
146
|
""
|
|
136
147
|
end
|
|
137
148
|
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
# Class-level caches
|
|
150
|
+
class << self
|
|
151
|
+
@cache_mutex = Mutex.new
|
|
152
|
+
|
|
153
|
+
# Cache aliases.json across the process, but invalidate when the file changes (dev-safe)
|
|
154
|
+
def icon_alias_map
|
|
155
|
+
return @icon_alias_map if alias_cache_fresh?
|
|
156
|
+
|
|
157
|
+
cache_mutex.synchronize do
|
|
158
|
+
return @icon_alias_map if alias_cache_fresh?
|
|
159
|
+
|
|
160
|
+
@icon_alias_map =
|
|
161
|
+
if Rails.application.config.respond_to?(:icon_alias_path)
|
|
162
|
+
base_path = Rails.application.config.icon_alias_path
|
|
163
|
+
full_path = Rails.root.join(base_path)
|
|
164
|
+
@icon_alias_map_mtime = safe_mtime(full_path)
|
|
165
|
+
|
|
166
|
+
json = File.read(full_path)
|
|
167
|
+
parsed = JSON.parse(json)
|
|
168
|
+
parsed.fetch("aliases", {}).freeze
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
@icon_alias_map
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Cache an index of icon_name to file path for all SVGs in the configured directory, with invalidation based on directory mtime
|
|
176
|
+
# Avoids recursive Dir.glob for every icon render
|
|
177
|
+
def icon_path_index
|
|
178
|
+
return @icon_path_index if index_cache_fresh?
|
|
179
|
+
|
|
180
|
+
cache_mutex.synchronize do
|
|
181
|
+
return @icon_path_index if index_cache_fresh?
|
|
182
|
+
|
|
183
|
+
@icon_path_index =
|
|
184
|
+
if Rails.application.config.respond_to?(:icon_path)
|
|
185
|
+
base_path = Rails.application.config.icon_path
|
|
186
|
+
root = Rails.root.join(base_path)
|
|
187
|
+
|
|
188
|
+
# If path doesn't exist, keep behavior aligned (no path resolution)
|
|
189
|
+
if Dir.exist?(root)
|
|
190
|
+
@icon_path_index_cache_key = icon_path_cache_key(root)
|
|
191
|
+
|
|
192
|
+
# One scan builds the map for O(1) lookups
|
|
193
|
+
# Key is the filename (without .svg) to match existing usage
|
|
194
|
+
index = {}
|
|
195
|
+
Dir.glob(File.join(root.to_s, "**", "*.svg")).sort.each do |p|
|
|
196
|
+
name = File.basename(p, ".svg")
|
|
197
|
+
index[name] ||= p
|
|
198
|
+
end
|
|
199
|
+
index.freeze
|
|
200
|
+
else
|
|
201
|
+
@icon_path_index_cache_key = nil
|
|
202
|
+
{}
|
|
203
|
+
end
|
|
204
|
+
else
|
|
205
|
+
{}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
@icon_path_index_checked_at = monotonic_now
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
@icon_path_index
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
def cache_mutex
|
|
217
|
+
@cache_mutex ||= Mutex.new
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def alias_cache_fresh?
|
|
221
|
+
return false unless defined?(@icon_alias_map)
|
|
222
|
+
|
|
223
|
+
return true unless Rails.application.config.respond_to?(:icon_alias_path)
|
|
224
|
+
|
|
225
|
+
full_path = Rails.root.join(Rails.application.config.icon_alias_path)
|
|
226
|
+
safe_mtime(full_path) == @icon_alias_map_mtime
|
|
227
|
+
rescue
|
|
228
|
+
false
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def index_cache_fresh?
|
|
232
|
+
return false unless defined?(@icon_path_index)
|
|
233
|
+
|
|
234
|
+
return true unless Rails.application.config.respond_to?(:icon_path)
|
|
235
|
+
|
|
236
|
+
# In development and production, skip re-checks for a short TTL window
|
|
237
|
+
# to avoid repeated tree scans on hot paths.
|
|
238
|
+
return true if Rails.env.development? && within_icon_index_ttl?(ICON_PATH_DEV_CACHE_TTL_SECONDS)
|
|
239
|
+
return true if Rails.env.production? && within_icon_index_ttl?(ICON_PATH_PROD_CACHE_TTL_SECONDS)
|
|
240
|
+
|
|
241
|
+
root = Rails.root.join(Rails.application.config.icon_path)
|
|
242
|
+
fresh = icon_path_cache_key(root) == @icon_path_index_cache_key
|
|
243
|
+
@icon_path_index_checked_at = monotonic_now
|
|
244
|
+
fresh
|
|
245
|
+
rescue
|
|
246
|
+
false
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def within_icon_index_ttl?(ttl_seconds)
|
|
250
|
+
return false unless defined?(@icon_path_index_checked_at)
|
|
251
|
+
|
|
252
|
+
(monotonic_now - @icon_path_index_checked_at) < ttl_seconds
|
|
253
|
+
rescue
|
|
254
|
+
false
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def monotonic_now
|
|
258
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def icon_path_cache_key(root)
|
|
262
|
+
return safe_mtime(root) unless Rails.env.development? || Rails.env.production?
|
|
263
|
+
|
|
264
|
+
digest = Digest::SHA1.new
|
|
265
|
+
root_prefix = "#{root}/"
|
|
266
|
+
|
|
267
|
+
Dir.glob(File.join(root.to_s, "**", "*.svg")).sort.each do |path|
|
|
268
|
+
digest << path.delete_prefix(root_prefix)
|
|
269
|
+
next unless Rails.env.development?
|
|
270
|
+
|
|
271
|
+
# Development tracks file metadata for rapid local edits.
|
|
272
|
+
# Production only needs path-set change detection during periodic checks.
|
|
273
|
+
stat = File.stat(path)
|
|
274
|
+
digest << stat.mtime.to_f.to_s
|
|
275
|
+
digest << stat.size.to_s
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
digest.hexdigest
|
|
279
|
+
rescue
|
|
280
|
+
nil
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def safe_mtime(path)
|
|
284
|
+
File.exist?(path) ? File.mtime(path) : nil
|
|
285
|
+
rescue
|
|
286
|
+
nil
|
|
287
|
+
end
|
|
140
288
|
end
|
|
141
289
|
|
|
142
290
|
private
|
|
143
291
|
|
|
144
292
|
def resolve_alias(icon)
|
|
145
293
|
return icon unless icon_alias_map
|
|
294
|
+
return icon if icon.nil?
|
|
146
295
|
|
|
147
296
|
aliases = icon_alias_map[icon]
|
|
148
297
|
return icon unless aliases
|
|
@@ -155,8 +304,8 @@ module Playbook
|
|
|
155
304
|
end
|
|
156
305
|
|
|
157
306
|
def file_exists?(alias_name)
|
|
158
|
-
|
|
159
|
-
|
|
307
|
+
# Use the cached index (no recursive glob)
|
|
308
|
+
self.class.icon_path_index.key?(alias_name)
|
|
160
309
|
end
|
|
161
310
|
|
|
162
311
|
def svg_size
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import MultiLevelSelect from "../_multi_level_select";
|
|
3
|
+
import { Button } from "playbook-ui";
|
|
4
|
+
|
|
5
|
+
const treeData = [
|
|
6
|
+
{
|
|
7
|
+
label: "Power Home Remodeling",
|
|
8
|
+
value: "powerHomeRemodeling",
|
|
9
|
+
id: "100",
|
|
10
|
+
expanded: true,
|
|
11
|
+
children: [
|
|
12
|
+
{
|
|
13
|
+
label: "People",
|
|
14
|
+
value: "people",
|
|
15
|
+
id: "101",
|
|
16
|
+
expanded: true,
|
|
17
|
+
children: [
|
|
18
|
+
{
|
|
19
|
+
label: "Talent Acquisition",
|
|
20
|
+
value: "talentAcquisition",
|
|
21
|
+
id: "102",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "Business Affairs",
|
|
25
|
+
value: "businessAffairs",
|
|
26
|
+
id: "103",
|
|
27
|
+
children: [
|
|
28
|
+
{
|
|
29
|
+
label: "Initiatives",
|
|
30
|
+
value: "initiatives",
|
|
31
|
+
id: "104",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: "Learning & Development",
|
|
35
|
+
value: "learningAndDevelopment",
|
|
36
|
+
id: "105",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: "People Experience",
|
|
42
|
+
value: "peopleExperience",
|
|
43
|
+
id: "106",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: "Contact Center",
|
|
49
|
+
value: "contactCenter",
|
|
50
|
+
id: "107",
|
|
51
|
+
children: [
|
|
52
|
+
{
|
|
53
|
+
label: "Appointment Management",
|
|
54
|
+
value: "appointmentManagement",
|
|
55
|
+
id: "108",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: "Customer Service",
|
|
59
|
+
value: "customerService",
|
|
60
|
+
id: "109",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: "Energy",
|
|
64
|
+
value: "energy",
|
|
65
|
+
id: "110",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const MultiLevelSelectReactResetKey = (props) => {
|
|
74
|
+
const [resetKey, setResetKey] = useState(0);
|
|
75
|
+
|
|
76
|
+
const handleReset = () => {
|
|
77
|
+
setResetKey((k) => k + 1);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<MultiLevelSelect
|
|
83
|
+
{...props}
|
|
84
|
+
id="multi-level-select-reset-example"
|
|
85
|
+
key={`multi-level-select-reset-${resetKey}`}
|
|
86
|
+
name="my_array"
|
|
87
|
+
returnAllSelected
|
|
88
|
+
treeData={treeData}
|
|
89
|
+
/>
|
|
90
|
+
<Button
|
|
91
|
+
id="multilevelselect-reset-button"
|
|
92
|
+
marginTop="lg"
|
|
93
|
+
onClick={handleReset}
|
|
94
|
+
text="Reset"
|
|
95
|
+
/>
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default MultiLevelSelectReactResetKey;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
When a parent resets a Multi Level Select (e.g., “Default” or “Clear”), the kit needs a `key` that changes with the selection because React uses the `key` to determine component identity and whether to preserve internal state. If the `key` doesn’t change, React reuses the existing instance and may keep showing the old selection instead of resetting to the new one.
|
|
@@ -39,3 +39,4 @@ examples:
|
|
|
39
39
|
- multi_level_select_disabled_options_parent: Disabled Parent Option (Return All Selected)
|
|
40
40
|
- multi_level_select_single_disabled: Disabled Options (Single Select)
|
|
41
41
|
- multi_level_select_required_indicator: Required Indicator
|
|
42
|
+
- multi_level_select_react_reset_key: Reset with Key (React)
|
|
@@ -17,3 +17,4 @@ export { default as MultiLevelSelectDisabledOptionsDefault } from "./_multi_leve
|
|
|
17
17
|
export { default as MultiLevelSelectLabel } from "./_multi_level_select_label.jsx"
|
|
18
18
|
export { default as MultiLevelSelectSingleDisabled } from "./_multi_level_select_single_disabled.jsx"
|
|
19
19
|
export { default as MultiLevelSelectRequiredIndicator } from "./_multi_level_select_required_indicator.jsx"
|
|
20
|
+
export { default as MultiLevelSelectReactResetKey } from "./_multi_level_select_react_reset_key.jsx"
|
|
@@ -73,8 +73,9 @@ $top_bottom_radius: 0px;
|
|
|
73
73
|
}
|
|
74
74
|
.disabled {
|
|
75
75
|
pointer-events: none;
|
|
76
|
+
cursor: not-allowed;
|
|
76
77
|
opacity: 0.5;
|
|
77
|
-
color:
|
|
78
|
+
color: #B0BDC7; // replace with $text_disabled once added to tokens
|
|
78
79
|
|
|
79
80
|
& > span {
|
|
80
81
|
padding: $pagination_padding;
|
|
@@ -104,4 +105,103 @@ $top_bottom_radius: 0px;
|
|
|
104
105
|
border-right: none;
|
|
105
106
|
margin-left: $space_xxs;
|
|
106
107
|
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.pb_pagination_mobile {
|
|
111
|
+
padding: $space_xxs 0px !important;
|
|
112
|
+
position: relative;
|
|
113
|
+
|
|
114
|
+
.pagination-right {
|
|
115
|
+
border-left: none !important;
|
|
116
|
+
padding: 6px 13px !important;
|
|
117
|
+
flex-shrink: 0;
|
|
118
|
+
|
|
119
|
+
@media (max-width: 435px) {
|
|
120
|
+
padding: 6px 10px !important;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
.pagination-left {
|
|
124
|
+
border-right: none !important;
|
|
125
|
+
padding: 6px 13px !important;
|
|
126
|
+
flex-shrink: 0;
|
|
127
|
+
|
|
128
|
+
@media (max-width: 435px) {
|
|
129
|
+
padding: 6px 10px !important;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
.pagination-dropdown {
|
|
133
|
+
position: static;
|
|
134
|
+
flex: 1;
|
|
135
|
+
margin: 0 $space_xxs;
|
|
136
|
+
min-width: 0;
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.pagination-dropdown-trigger {
|
|
141
|
+
padding: 6px 10px 6px 13px !important;
|
|
142
|
+
background-color: $bg_light;
|
|
143
|
+
border-radius: $border_rad_light;
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
transition: all 0.2s ease;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
|
|
149
|
+
&:hover {
|
|
150
|
+
background-color: $active_light;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@media (max-width: 435px) {
|
|
154
|
+
padding: 6px 8px 6px 10px !important;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.pagination-dropdown-menu {
|
|
159
|
+
position: absolute;
|
|
160
|
+
left: 0;
|
|
161
|
+
right: 0;
|
|
162
|
+
background-color: $white;
|
|
163
|
+
border: $border_rad_lightest solid $border_light;
|
|
164
|
+
border-radius: $border_rad_light;
|
|
165
|
+
box-shadow: $shadow_deep;
|
|
166
|
+
max-height: 200px;
|
|
167
|
+
overflow-y: auto;
|
|
168
|
+
z-index: 1000;
|
|
169
|
+
|
|
170
|
+
&.below {
|
|
171
|
+
top: 100%;
|
|
172
|
+
margin-top: $space_xs;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
&.above {
|
|
176
|
+
bottom: 100%;
|
|
177
|
+
margin-bottom: $space_xs;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.pagination-dropdown-option {
|
|
182
|
+
padding: $pagination_padding;
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
transition: background-color 0.2s ease;
|
|
185
|
+
|
|
186
|
+
&:hover {
|
|
187
|
+
background-color: $active_light;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
&.active {
|
|
191
|
+
background-color: $primary;
|
|
192
|
+
.pb_body_kit {
|
|
193
|
+
color: $white;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
&:first-child {
|
|
198
|
+
border-top-left-radius: $border_rad_light;
|
|
199
|
+
border-top-right-radius: $border_rad_light;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
&:last-child {
|
|
203
|
+
border-bottom-left-radius: $border_rad_light;
|
|
204
|
+
border-bottom-right-radius: $border_rad_light;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
107
207
|
}
|
|
@@ -209,4 +209,175 @@ describe('Pagination Component', () => {
|
|
|
209
209
|
|
|
210
210
|
expect(screen.getByText('19')).toBeInTheDocument()
|
|
211
211
|
})
|
|
212
|
-
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('Pagination Mobile View', () => {
|
|
215
|
+
let originalInnerWidth
|
|
216
|
+
|
|
217
|
+
beforeEach(() => {
|
|
218
|
+
// Store original value
|
|
219
|
+
originalInnerWidth = window.innerWidth
|
|
220
|
+
|
|
221
|
+
// Mock window.innerWidth for mobile
|
|
222
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
223
|
+
writable: true,
|
|
224
|
+
configurable: true,
|
|
225
|
+
value: 767
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
afterEach(() => {
|
|
230
|
+
// Restore original value
|
|
231
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
232
|
+
writable: true,
|
|
233
|
+
configurable: true,
|
|
234
|
+
value: originalInnerWidth
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test('renders mobile layout on small screens', () => {
|
|
239
|
+
render(<Pagination {...defaultProps} />)
|
|
240
|
+
|
|
241
|
+
const mobilePagination = document.querySelector('.pb_pagination_mobile')
|
|
242
|
+
expect(mobilePagination).toBeInTheDocument()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('renders Prev and Next buttons in mobile view', () => {
|
|
246
|
+
render(<Pagination {...defaultProps} />)
|
|
247
|
+
|
|
248
|
+
expect(screen.getByText('Prev')).toBeInTheDocument()
|
|
249
|
+
expect(screen.getByText('Next')).toBeInTheDocument()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('displays current page and total in dropdown trigger', () => {
|
|
253
|
+
render(<Pagination {...defaultProps}
|
|
254
|
+
current={3}
|
|
255
|
+
total={10}
|
|
256
|
+
/>)
|
|
257
|
+
|
|
258
|
+
expect(screen.getByText('3', { exact: false })).toBeInTheDocument()
|
|
259
|
+
expect(screen.getByText(/of 10/)).toBeInTheDocument()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('opens dropdown when trigger is clicked', () => {
|
|
263
|
+
render(<Pagination {...defaultProps} />)
|
|
264
|
+
|
|
265
|
+
const dropdownTrigger = document.querySelector('.pagination-dropdown-trigger')
|
|
266
|
+
expect(dropdownTrigger).toBeInTheDocument()
|
|
267
|
+
|
|
268
|
+
let dropdownMenu = document.querySelector('.pagination-dropdown-menu')
|
|
269
|
+
expect(dropdownMenu).not.toBeInTheDocument()
|
|
270
|
+
|
|
271
|
+
fireEvent.click(dropdownTrigger)
|
|
272
|
+
|
|
273
|
+
dropdownMenu = document.querySelector('.pagination-dropdown-menu')
|
|
274
|
+
expect(dropdownMenu).toBeInTheDocument()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('displays all page options in dropdown', () => {
|
|
278
|
+
render(<Pagination {...defaultProps}
|
|
279
|
+
total={5}
|
|
280
|
+
/>)
|
|
281
|
+
|
|
282
|
+
const dropdownTrigger = document.querySelector('.pagination-dropdown-trigger')
|
|
283
|
+
fireEvent.click(dropdownTrigger)
|
|
284
|
+
|
|
285
|
+
expect(screen.getByText('Page 1')).toBeInTheDocument()
|
|
286
|
+
expect(screen.getByText('Page 2')).toBeInTheDocument()
|
|
287
|
+
expect(screen.getByText('Page 3')).toBeInTheDocument()
|
|
288
|
+
expect(screen.getByText('Page 4')).toBeInTheDocument()
|
|
289
|
+
expect(screen.getByText('Page 5')).toBeInTheDocument()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('highlights current page in dropdown', () => {
|
|
293
|
+
render(<Pagination {...defaultProps}
|
|
294
|
+
current={3}
|
|
295
|
+
total={5}
|
|
296
|
+
/>)
|
|
297
|
+
|
|
298
|
+
const dropdownTrigger = document.querySelector('.pagination-dropdown-trigger')
|
|
299
|
+
fireEvent.click(dropdownTrigger)
|
|
300
|
+
|
|
301
|
+
const activePage = screen.getByText('Page 3').parentElement
|
|
302
|
+
expect(activePage).toHaveClass('active')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('changes page when dropdown option is clicked', () => {
|
|
306
|
+
const mockOnChange = jest.fn()
|
|
307
|
+
render(<Pagination {...defaultProps}
|
|
308
|
+
onChange={mockOnChange}
|
|
309
|
+
total={5}
|
|
310
|
+
/>)
|
|
311
|
+
|
|
312
|
+
const dropdownTrigger = document.querySelector('.pagination-dropdown-trigger')
|
|
313
|
+
fireEvent.click(dropdownTrigger)
|
|
314
|
+
|
|
315
|
+
const page3Option = screen.getByText('Page 3')
|
|
316
|
+
fireEvent.click(page3Option)
|
|
317
|
+
|
|
318
|
+
expect(mockOnChange).toHaveBeenCalledWith(3)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('closes dropdown after selecting a page', () => {
|
|
322
|
+
render(<Pagination {...defaultProps}
|
|
323
|
+
total={5}
|
|
324
|
+
/>)
|
|
325
|
+
|
|
326
|
+
const dropdownTrigger = document.querySelector('.pagination-dropdown-trigger')
|
|
327
|
+
fireEvent.click(dropdownTrigger)
|
|
328
|
+
|
|
329
|
+
let dropdownMenu = document.querySelector('.pagination-dropdown-menu')
|
|
330
|
+
expect(dropdownMenu).toBeInTheDocument()
|
|
331
|
+
|
|
332
|
+
const page3Option = screen.getByText('Page 3')
|
|
333
|
+
fireEvent.click(page3Option)
|
|
334
|
+
|
|
335
|
+
dropdownMenu = document.querySelector('.pagination-dropdown-menu')
|
|
336
|
+
expect(dropdownMenu).not.toBeInTheDocument()
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('Prev button navigates to previous page', () => {
|
|
340
|
+
const mockOnChange = jest.fn()
|
|
341
|
+
render(<Pagination {...defaultProps}
|
|
342
|
+
current={3}
|
|
343
|
+
onChange={mockOnChange}
|
|
344
|
+
/>)
|
|
345
|
+
|
|
346
|
+
const prevButton = document.querySelector('.pagination-left')
|
|
347
|
+
fireEvent.click(prevButton)
|
|
348
|
+
|
|
349
|
+
expect(mockOnChange).toHaveBeenCalledWith(2)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
test('Next button navigates to next page', () => {
|
|
353
|
+
const mockOnChange = jest.fn()
|
|
354
|
+
render(<Pagination {...defaultProps}
|
|
355
|
+
current={3}
|
|
356
|
+
onChange={mockOnChange}
|
|
357
|
+
/>)
|
|
358
|
+
|
|
359
|
+
const nextButton = document.querySelector('.pagination-right')
|
|
360
|
+
fireEvent.click(nextButton)
|
|
361
|
+
|
|
362
|
+
expect(mockOnChange).toHaveBeenCalledWith(4)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('disables Prev button on first page', () => {
|
|
366
|
+
render(<Pagination {...defaultProps}
|
|
367
|
+
current={1}
|
|
368
|
+
/>)
|
|
369
|
+
|
|
370
|
+
const prevButton = document.querySelector('.pagination-left')
|
|
371
|
+
expect(prevButton).toHaveClass('disabled')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test('disables Next button on last page', () => {
|
|
375
|
+
render(<Pagination {...defaultProps}
|
|
376
|
+
current={10}
|
|
377
|
+
/>)
|
|
378
|
+
|
|
379
|
+
const nextButton = document.querySelector('.pagination-right')
|
|
380
|
+
expect(nextButton).toHaveClass('disabled')
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
})
|