stimulus_plumbers 0.4.15 → 0.4.17
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/CHANGELOG.md +21 -0
- data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +378 -1
- data/app/assets/javascripts/stimulus-plumbers/index.es.js +196 -163
- data/app/assets/javascripts/stimulus-plumbers/index.es.js.map +1 -1
- data/app/assets/javascripts/stimulus-plumbers/index.umd.js +1 -1
- data/app/assets/javascripts/stimulus-plumbers/index.umd.js.map +1 -1
- data/docs/component/combobox.md +3 -3
- data/docs/component/form.md +49 -14
- data/docs/component/indicator.md +1 -1
- data/docs/component/progress.md +24 -9
- data/docs/guide.md +63 -4
- data/lib/stimulus_plumbers/components/combobox/dropdown.rb +1 -1
- data/lib/stimulus_plumbers/components/combobox/time.rb +1 -1
- data/lib/stimulus_plumbers/components/combobox.rb +1 -1
- data/lib/stimulus_plumbers/components/progress/shared.rb +46 -3
- data/lib/stimulus_plumbers/components/progress_bar.rb +100 -22
- data/lib/stimulus_plumbers/components/progress_ring.rb +3 -2
- data/lib/stimulus_plumbers/form/builder.rb +20 -4
- data/lib/stimulus_plumbers/form/field.rb +34 -6
- data/lib/stimulus_plumbers/form/fields/inputs/progress.rb +30 -0
- data/lib/stimulus_plumbers/form/fields/inputs/range.rb +81 -0
- data/lib/stimulus_plumbers/form/fields/inputs/text.rb +1 -1
- data/lib/stimulus_plumbers/form/fields/renderer.rb +41 -0
- data/lib/stimulus_plumbers/helpers/progress_helper.rb +4 -2
- data/lib/stimulus_plumbers/themes/schema/progress/ranges.rb +13 -0
- data/lib/stimulus_plumbers/themes/schema.rb +17 -6
- data/lib/stimulus_plumbers/version.rb +1 -1
- data/vendor/ARIA.md +10 -0
- data/vendor/component/manifest.json +18 -6
- data/vendor/controller/docs/progress.md +69 -9
- data/vendor/controller/guide.md +61 -7
- data/vendor/controller/manifest.json +378 -1
- metadata +4 -1
|
@@ -13,26 +13,40 @@ module StimulusPlumbers
|
|
|
13
13
|
COLLECTION_TYPES = %i[radio check_box collection_select grouped_collection_select].freeze
|
|
14
14
|
OPTIONS = (Base::OPTIONS + %i[hide_label]).freeze
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# :aria uses aria-labelledby because non-labelable components cannot use `<label for>`.
|
|
17
|
+
LABEL_MODES = %i[native aria].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :hide_label, :label_mode
|
|
17
20
|
|
|
18
21
|
class << self
|
|
19
22
|
def label_id(input_id)
|
|
20
23
|
[input_id, "label"].compact.join("_")
|
|
21
24
|
end
|
|
25
|
+
|
|
26
|
+
def validate_label_mode!(mode)
|
|
27
|
+
return mode if LABEL_MODES.include?(mode)
|
|
28
|
+
|
|
29
|
+
raise ArgumentError, "unknown label_mode: #{mode.inspect} (expected one of #{LABEL_MODES.join(", ")})"
|
|
30
|
+
end
|
|
22
31
|
end
|
|
23
32
|
|
|
24
|
-
def initialize(template, hide_label: false, **kwargs)
|
|
33
|
+
def initialize(template, hide_label: false, label_mode: :native, **kwargs)
|
|
25
34
|
super(template, **kwargs)
|
|
26
35
|
@hide_label = hide_label
|
|
36
|
+
@label_mode = self.class.validate_label_mode!(label_mode)
|
|
27
37
|
end
|
|
28
38
|
|
|
29
39
|
def label_hidden?
|
|
30
40
|
@hide_label
|
|
31
41
|
end
|
|
32
42
|
|
|
43
|
+
def native_label?
|
|
44
|
+
@label_mode == :native
|
|
45
|
+
end
|
|
46
|
+
|
|
33
47
|
def render(object, attribute, input_id:, &block)
|
|
34
48
|
@label ||= attribute.to_s.humanize
|
|
35
|
-
case @floating
|
|
49
|
+
case native_label? ? @floating : nil
|
|
36
50
|
when *StimulusPlumbers::Themes::Schema::Form::Floating::Ranges::TYPE
|
|
37
51
|
render_floating_field(object, attribute, input_id, &block)
|
|
38
52
|
else
|
|
@@ -42,6 +56,18 @@ module StimulusPlumbers
|
|
|
42
56
|
|
|
43
57
|
private
|
|
44
58
|
|
|
59
|
+
def build_aria(object, attribute, input_id)
|
|
60
|
+
return super if native_label?
|
|
61
|
+
|
|
62
|
+
{ describedby: described_by(object, attribute, input_id), labelledby: self.class.label_id(input_id) }.compact
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def build_html_options(input_id, aria)
|
|
66
|
+
return super if native_label?
|
|
67
|
+
|
|
68
|
+
{ id: input_id, aria: aria }
|
|
69
|
+
end
|
|
70
|
+
|
|
45
71
|
def render_default_field(object, attribute, input_id, &block)
|
|
46
72
|
error_override = error?(object, attribute)
|
|
47
73
|
aria = build_aria(object, attribute, input_id)
|
|
@@ -59,13 +85,15 @@ module StimulusPlumbers
|
|
|
59
85
|
end
|
|
60
86
|
end
|
|
61
87
|
|
|
88
|
+
# hide_label is visual only; :aria fields cannot be required.
|
|
62
89
|
def field_label(input_id)
|
|
63
90
|
Fields::Label.new(@template).render(
|
|
64
91
|
text: @label,
|
|
65
|
-
for_id: input_id,
|
|
92
|
+
for_id: (input_id if native_label?),
|
|
66
93
|
id: self.class.label_id(input_id),
|
|
67
|
-
required: @required,
|
|
68
|
-
hidden: @hide_label
|
|
94
|
+
required: native_label? && @required,
|
|
95
|
+
hidden: @hide_label,
|
|
96
|
+
tag: native_label? ? :label : :span
|
|
69
97
|
)
|
|
70
98
|
end
|
|
71
99
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Form
|
|
5
|
+
module Fields
|
|
6
|
+
module Inputs
|
|
7
|
+
module Progress
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# A progressbar submits nothing and is never invalid — it reads its value from the model
|
|
11
|
+
# attribute, and `floating:` is dropped so it can't leak onto the element as an attribute.
|
|
12
|
+
def render_progress(attribute, html_opts, opts, _error, segments: nil, format: nil, readout: :inside, **kwargs)
|
|
13
|
+
html_options = merge_html_options(
|
|
14
|
+
theme.resolve(:form_field_input_progress), opts, html_opts, kwargs.except(:floating)
|
|
15
|
+
)
|
|
16
|
+
value = object.public_send(attribute)
|
|
17
|
+
component = Components::ProgressBar.new(@template)
|
|
18
|
+
if segments
|
|
19
|
+
raise ArgumentError, "format: is not supported with segments:" unless format.nil?
|
|
20
|
+
|
|
21
|
+
component.render_segmented(value: value, segments: segments, **html_options)
|
|
22
|
+
else
|
|
23
|
+
component.render(value: value, format: format, readout: readout, **html_options)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Form
|
|
5
|
+
module Fields
|
|
6
|
+
module Inputs
|
|
7
|
+
# A range is a track and a thumb, not a text box, so it takes none of the text input's
|
|
8
|
+
# chrome. With `format:` it grows a readout driven by the progress controller.
|
|
9
|
+
module Range
|
|
10
|
+
include Components::Progress::Shared
|
|
11
|
+
|
|
12
|
+
def range_field(attribute, **options)
|
|
13
|
+
super(attribute, merge_html_options(theme.resolve(:form_field_input_range), options.except(:floating)))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# `floating:` is dropped — a label can't sit inside a slider.
|
|
19
|
+
def render_range_input(attribute, html_opts, opts, _error, format: nil, min: 0, max: 100, **kwargs)
|
|
20
|
+
validate_format!(format)
|
|
21
|
+
current = clamp(numeric(object.public_send(attribute)), min, max)
|
|
22
|
+
input = range_input(attribute, current, min, max, format, html_opts, opts, kwargs.except(:floating))
|
|
23
|
+
return input if format.nil?
|
|
24
|
+
|
|
25
|
+
range_group(input, current, min, max, format)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# No readout to contain, so the input hosts the controller itself.
|
|
29
|
+
def range_input(attribute, current, min, max, format, html_opts, opts, kwargs)
|
|
30
|
+
wired = format.nil? ? range_stimulus_data(current, min, max, format) : { data: { "progress-target": "input" } }
|
|
31
|
+
html_options = merge_html_options(
|
|
32
|
+
theme.resolve(:form_field_input_range),
|
|
33
|
+
opts,
|
|
34
|
+
html_opts,
|
|
35
|
+
kwargs,
|
|
36
|
+
wired,
|
|
37
|
+
# The input carries the track gradient, so the fill percentage lands here, not on
|
|
38
|
+
# the wrapper. Server-rendered so the fill is right before the controller connects.
|
|
39
|
+
{ min: min, max: max, style: "--sp-progress-percent: #{integral(percent(current, min, max).round(2))}" }
|
|
40
|
+
)
|
|
41
|
+
@template.range_field(@object_name, attribute, objectify_options(html_options))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A Stimulus target must be a descendant of its controller element, so a readout
|
|
45
|
+
# forces a wrapper local to the input row.
|
|
46
|
+
def range_group(input, current, min, max, format)
|
|
47
|
+
html_options = merge_html_options(
|
|
48
|
+
theme.resolve(:form_field_input_range_group),
|
|
49
|
+
range_stimulus_data(current, min, max, format)
|
|
50
|
+
)
|
|
51
|
+
body = @template.safe_join([input, range_value(format, current, min, max)])
|
|
52
|
+
@template.content_tag(:div, body, **html_options)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# aria-hidden: the native input already announces its own value.
|
|
56
|
+
def range_value(format, current, min, max)
|
|
57
|
+
@template.content_tag(
|
|
58
|
+
:span,
|
|
59
|
+
value_text(format, current, min, max),
|
|
60
|
+
**merge_html_options(
|
|
61
|
+
theme.resolve(:form_field_input_range_value),
|
|
62
|
+
{ data: { "progress-target": "value" }, aria: { hidden: true } }
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def range_stimulus_data(current, min, max, format)
|
|
68
|
+
progress_stimulus_data(
|
|
69
|
+
value: current,
|
|
70
|
+
min: min,
|
|
71
|
+
max: max,
|
|
72
|
+
variant: "range",
|
|
73
|
+
action: "input->progress#refresh",
|
|
74
|
+
**(format.nil? ? {} : { "progress-format-value": format })
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -5,6 +5,7 @@ module StimulusPlumbers
|
|
|
5
5
|
module Fields
|
|
6
6
|
module Inputs
|
|
7
7
|
module Text
|
|
8
|
+
# `range` is not here — see inputs/range.rb; it shares none of the text input chrome.
|
|
8
9
|
TEXT_FIELD_METHODS = {
|
|
9
10
|
text: :text_field,
|
|
10
11
|
email: :email_field,
|
|
@@ -14,7 +15,6 @@ module StimulusPlumbers
|
|
|
14
15
|
color: :color_field,
|
|
15
16
|
month: :month_field,
|
|
16
17
|
week: :week_field,
|
|
17
|
-
range: :range_field,
|
|
18
18
|
datetime_local: :datetime_local_field
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../field"
|
|
4
|
+
|
|
3
5
|
module StimulusPlumbers
|
|
4
6
|
module Form
|
|
5
7
|
module Fields
|
|
@@ -18,6 +20,7 @@ module StimulusPlumbers
|
|
|
18
20
|
text_area: :render_text_area_input,
|
|
19
21
|
file: :render_file_input,
|
|
20
22
|
password: :render_password_input,
|
|
23
|
+
progress: :render_progress,
|
|
21
24
|
code: :render_code_input,
|
|
22
25
|
credit_card: :render_credit_card_input,
|
|
23
26
|
date: :render_combobox_date,
|
|
@@ -26,6 +29,44 @@ module StimulusPlumbers
|
|
|
26
29
|
search: :render_combobox_typeahead
|
|
27
30
|
}.freeze
|
|
28
31
|
|
|
32
|
+
# Require every field type to declare its caption association.
|
|
33
|
+
LABEL_MODE = {
|
|
34
|
+
text: :native,
|
|
35
|
+
email: :native,
|
|
36
|
+
number: :native,
|
|
37
|
+
url: :native,
|
|
38
|
+
tel: :native,
|
|
39
|
+
color: :native,
|
|
40
|
+
month: :native,
|
|
41
|
+
week: :native,
|
|
42
|
+
range: :native,
|
|
43
|
+
datetime_local: :native,
|
|
44
|
+
text_area: :native,
|
|
45
|
+
file: :native,
|
|
46
|
+
password: :native,
|
|
47
|
+
progress: :aria,
|
|
48
|
+
code: :native,
|
|
49
|
+
credit_card: :native,
|
|
50
|
+
date: :native,
|
|
51
|
+
time: :native,
|
|
52
|
+
select: :native,
|
|
53
|
+
search: :native
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
56
|
+
missing = FIELD.keys - LABEL_MODE.keys
|
|
57
|
+
raise ArgumentError, "field types missing a label_mode: #{missing.join(", ")}" if missing.any?
|
|
58
|
+
|
|
59
|
+
extra = LABEL_MODE.keys - FIELD.keys
|
|
60
|
+
raise ArgumentError, "label_mode declared for unknown field types: #{extra.join(", ")}" if extra.any?
|
|
61
|
+
|
|
62
|
+
LABEL_MODE.each_value { |mode| Field.validate_label_mode!(mode) }
|
|
63
|
+
|
|
64
|
+
class << self
|
|
65
|
+
def label_mode(as)
|
|
66
|
+
LABEL_MODE.fetch(as)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
29
70
|
COLLECTION = {
|
|
30
71
|
collection_select: :render_collection_combobox_dropdown,
|
|
31
72
|
grouped_collection_select: :render_grouped_collection_combobox_dropdown
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
module StimulusPlumbers
|
|
4
4
|
module Helpers
|
|
5
5
|
module ProgressHelper
|
|
6
|
-
def sp_progress_bar(value:, min: 0, max: 100, indeterminate: false, **kwargs)
|
|
7
|
-
Components::ProgressBar.new(self).render(
|
|
6
|
+
def sp_progress_bar(value:, min: 0, max: 100, indeterminate: false, format: nil, readout: :inside, **kwargs)
|
|
7
|
+
Components::ProgressBar.new(self).render(
|
|
8
|
+
value: value, min: min, max: max, indeterminate: indeterminate, format: format, readout: readout, **kwargs
|
|
9
|
+
)
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def sp_progress_segmented(value:, segments:, min: 0, max: 100, mode: :discrete, indeterminate: false, ramp: nil, **kwargs)
|
|
@@ -11,6 +11,7 @@ require_relative "schema/form/ranges"
|
|
|
11
11
|
require_relative "schema/form/checkbox/ranges"
|
|
12
12
|
require_relative "schema/form/floating/ranges"
|
|
13
13
|
require_relative "schema/form/radio/ranges"
|
|
14
|
+
require_relative "schema/progress/ranges"
|
|
14
15
|
require_relative "schema/icon"
|
|
15
16
|
|
|
16
17
|
module StimulusPlumbers
|
|
@@ -179,6 +180,10 @@ module StimulusPlumbers
|
|
|
179
180
|
type: { default: :default, validate: Form::Radio::Ranges::TYPE },
|
|
180
181
|
variant: { default: :tertiary, validate: Form::Radio::Ranges::VARIANT }
|
|
181
182
|
}.freeze,
|
|
183
|
+
form_field_input_progress: {}.freeze,
|
|
184
|
+
form_field_input_range: {}.freeze,
|
|
185
|
+
form_field_input_range_group: {}.freeze,
|
|
186
|
+
form_field_input_range_value: {}.freeze,
|
|
182
187
|
form_field_input_combobox: {
|
|
183
188
|
error: { default: false, validate: Ranges::BOOL },
|
|
184
189
|
floating: { default: nil, validate: [nil, *Form::Floating::Ranges::TYPE] }
|
|
@@ -255,13 +260,19 @@ module StimulusPlumbers
|
|
|
255
260
|
popover: {}.freeze
|
|
256
261
|
}.freeze
|
|
257
262
|
|
|
263
|
+
# The outside readout gets its own keys, not parameters on existing ones — a theme method
|
|
264
|
+
# written before them has no keyword to accept.
|
|
258
265
|
PROGRESS = {
|
|
259
|
-
progress_bar:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
266
|
+
progress_bar: { labelled: { default: false, validate: Ranges::BOOL } }.freeze,
|
|
267
|
+
progress_bar_group: {}.freeze,
|
|
268
|
+
progress_bar_fill: {}.freeze,
|
|
269
|
+
progress_bar_value: {}.freeze,
|
|
270
|
+
progress_bar_value_outside: {}.freeze,
|
|
271
|
+
progress_segment_group: {}.freeze,
|
|
272
|
+
progress_segment: {}.freeze,
|
|
273
|
+
progress_segment_fill: {}.freeze,
|
|
274
|
+
progress_ring: { size: { default: nil, validate: %i[sm md lg] } }.freeze,
|
|
275
|
+
progress_meter: {}.freeze
|
|
265
276
|
}.freeze
|
|
266
277
|
|
|
267
278
|
TIMELINE = {
|
data/vendor/ARIA.md
CHANGED
|
@@ -97,6 +97,16 @@ Two helper classes handle keyboard navigation in controllers — see [`stimulus-
|
|
|
97
97
|
- Accepted tradeoff: because `indeterminate` has no HTML attribute, the server can only render the master's initial `checked` state for the all-true case; every other case (including mixed) renders unchecked and is corrected to `indeterminate` once the `checklist` controller connects — a brief, accepted flash for the mixed case only.
|
|
98
98
|
- Disabled (readonly) items are excluded from the master's aggregate and from bulk toggling — the `checklist` controller filters them out via their own `.disabled` property, mirroring their exclusion from tab order and AT interaction.
|
|
99
99
|
|
|
100
|
+
#### Progress (`progress_controller`, `sp_progress_*`)
|
|
101
|
+
- `role="progressbar"` is read-only and never focusable — it reports a value, it does not accept one. An interactive equivalent is a native `<input type="range">` (or `role="slider"`), not a progressbar with `tabindex`
|
|
102
|
+
- Value: `aria-valuemin`/`aria-valuemax` always; `aria-valuenow` omitted while indeterminate (an omitted `valuenow` is what signals "unknown progress" to AT)
|
|
103
|
+
- `aria-valuetext` only when the readout text is not derivable from `aria-valuenow` — set for the `value`/`value_max` formats, deliberately **not** for `percent`, where AT already computes the percentage and a duplicate would be announced twice
|
|
104
|
+
- On-screen readout is `aria-hidden="true"` — the value reaches AT through `aria-valuenow`/`aria-valuetext`, so exposing the span too would double-announce it
|
|
105
|
+
- Name: `aria-label` standalone, or `aria-labelledby` → a visible caption. `<label for>` cannot name a progressbar — `for=` is only valid against a labelable element (`button`, `input`, `meter`, `output`, `progress`, `select`, `textarea`), so a `<div role="progressbar">` targeted by one is silently left unnamed (WCAG 4.1.2). Form fields rendering a progressbar therefore emit a `<span>` caption, not a `<label>`
|
|
106
|
+
- No `aria-invalid`/`aria-required` on a progressbar — neither is supported on the role, and it submits nothing that could be invalid. Errors still attach via `aria-describedby`
|
|
107
|
+
- Segment slots are decorative (`aria-hidden="true"`) — the value is announced once, by the container
|
|
108
|
+
- The `range` variant drives a native `<input type="range">` and writes **no** ARIA: the native control already exposes slider role, value, and keyboard operation, and duplicating them announces worse than leaving them alone. It keeps an ordinary `<label for>` because an `input` is labelable
|
|
109
|
+
|
|
100
110
|
#### Avatar / Card / Icon
|
|
101
111
|
- Decorative images/icons: `aria-hidden="true"` or `alt=""`
|
|
102
112
|
- Meaningful images: descriptive `alt` text
|
|
@@ -66,7 +66,11 @@
|
|
|
66
66
|
"values": []
|
|
67
67
|
},
|
|
68
68
|
"combobox-date": {
|
|
69
|
-
"actions": [
|
|
69
|
+
"actions": [
|
|
70
|
+
"onDaySelect",
|
|
71
|
+
"onMonthSelect",
|
|
72
|
+
"onYearSelect"
|
|
73
|
+
],
|
|
70
74
|
"listens": [
|
|
71
75
|
"selected"
|
|
72
76
|
],
|
|
@@ -76,7 +80,7 @@
|
|
|
76
80
|
"combobox-dropdown": {
|
|
77
81
|
"actions": [
|
|
78
82
|
"onNavigate",
|
|
79
|
-
"
|
|
83
|
+
"onSelect"
|
|
80
84
|
],
|
|
81
85
|
"listens": [
|
|
82
86
|
"selected"
|
|
@@ -87,7 +91,7 @@
|
|
|
87
91
|
"combobox-time": {
|
|
88
92
|
"actions": [
|
|
89
93
|
"onNavigate",
|
|
90
|
-
"
|
|
94
|
+
"onSelect"
|
|
91
95
|
],
|
|
92
96
|
"listens": [
|
|
93
97
|
"selected"
|
|
@@ -117,7 +121,8 @@
|
|
|
117
121
|
},
|
|
118
122
|
"input-combobox": {
|
|
119
123
|
"actions": [
|
|
120
|
-
"onInput"
|
|
124
|
+
"onInput",
|
|
125
|
+
"onSelect"
|
|
121
126
|
],
|
|
122
127
|
"listens": [
|
|
123
128
|
"changed"
|
|
@@ -128,6 +133,7 @@
|
|
|
128
133
|
"input-formatter": {
|
|
129
134
|
"actions": [
|
|
130
135
|
"onBlur",
|
|
136
|
+
"onChange",
|
|
131
137
|
"onFocus",
|
|
132
138
|
"onInput"
|
|
133
139
|
],
|
|
@@ -166,6 +172,7 @@
|
|
|
166
172
|
"popover": {
|
|
167
173
|
"actions": [
|
|
168
174
|
"close",
|
|
175
|
+
"closeOnSelect",
|
|
169
176
|
"open",
|
|
170
177
|
"toggle"
|
|
171
178
|
],
|
|
@@ -174,14 +181,19 @@
|
|
|
174
181
|
"values": []
|
|
175
182
|
},
|
|
176
183
|
"progress": {
|
|
177
|
-
"actions": [
|
|
184
|
+
"actions": [
|
|
185
|
+
"refresh"
|
|
186
|
+
],
|
|
178
187
|
"listens": [],
|
|
179
188
|
"targets": [
|
|
180
189
|
"fill",
|
|
181
|
-
"
|
|
190
|
+
"input",
|
|
191
|
+
"meter",
|
|
192
|
+
"value"
|
|
182
193
|
],
|
|
183
194
|
"values": [
|
|
184
195
|
"current",
|
|
196
|
+
"format",
|
|
185
197
|
"high",
|
|
186
198
|
"indeterminate",
|
|
187
199
|
"low",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Progress
|
|
2
2
|
|
|
3
|
-
Value-driven progress indicator supporting
|
|
3
|
+
Value-driven progress indicator supporting five render variants: a linear bar, a segmented bar, an SVG ring, a native `<meter>`, and a native `<input type="range">`.
|
|
4
4
|
|
|
5
5
|
## Stimulus Identifier
|
|
6
6
|
|
|
@@ -12,12 +12,14 @@ Value-driven progress indicator supporting four render variants: a linear bar, a
|
|
|
12
12
|
| ------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
13
|
| `fill` | `<div>` (bar) / `<circle>` (ring) | Element whose `width` (bar) or `stroke-dasharray`/`stroke-dashoffset` (ring) is set. Segmented renders **one `fill` per segment**; the controller distributes the value across them |
|
|
14
14
|
| `meter` | `<meter>` | Present only for `variant: "meter"` — native element, attributes synced directly |
|
|
15
|
+
| `value` | `<span>` | Optional on-screen readout; its `textContent` is set from `format`. Bar and range variants |
|
|
16
|
+
| `input` | `<input type="range">` | Range-only, and only when a readout is present — the controller then sits on a wrapper containing both |
|
|
15
17
|
|
|
16
18
|
## Values
|
|
17
19
|
|
|
18
20
|
| Name | Type | Default | Purpose |
|
|
19
21
|
| ----------------------- | ------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
-
| `variant` | String | `"bar"` | `"bar"` \| `"segmented"` \| `"ring"` \| `"meter"`
|
|
22
|
+
| `variant` | String | `"bar"` | `"bar"` \| `"segmented"` \| `"ring"` \| `"meter"` \| `"range"` |
|
|
21
23
|
| `current` | Number | `0` | Current value |
|
|
22
24
|
| `min` | Number | `0` | Range minimum |
|
|
23
25
|
| `max` | Number | `100` | Range maximum |
|
|
@@ -27,19 +29,37 @@ Value-driven progress indicator supporting four render variants: a linear bar, a
|
|
|
27
29
|
| `indeterminate` | Boolean | `false` | Suppresses `aria-valuenow`; toggles the `sp-progress-indeterminate` class |
|
|
28
30
|
| `indeterminateFraction` | Number | `0.25` | Bar width / ring arc / segment chunk-width fraction rendered while indeterminate |
|
|
29
31
|
| `segmentMode` | String | `"discrete"` | Segmented-only. `"discrete"` lights a whole segment once progress reaches into it; `"continuous"` partially fills the boundary segment |
|
|
32
|
+
| `format` | String | `""` | Readout text for the `value` target: `"percent"` \| `"value"` \| `"value_max"`. Empty or unrecognized renders nothing |
|
|
33
|
+
|
|
34
|
+
### Readout formats
|
|
35
|
+
|
|
36
|
+
| `format` | Renders | `aria-valuetext` |
|
|
37
|
+
| ------------- | --------- | ---------------- |
|
|
38
|
+
| `"percent"` | `45%` | not set |
|
|
39
|
+
| `"value"` | `45` | `45` |
|
|
40
|
+
| `"value_max"` | `45 / 60` | `45 / 60` |
|
|
41
|
+
|
|
42
|
+
`percent` omits `aria-valuetext` — assistive technology derives the percentage from `aria-valuenow` itself, so setting it would only duplicate what AT already announces. The readout element is decorative (`aria-hidden`); the value reaches AT through `aria-valuenow`/`aria-valuetext`.
|
|
43
|
+
|
|
44
|
+
The `range` variant writes no `aria-value*` at all — a native `<input type="range">` already exposes its own slider semantics — and sets `--sp-progress-percent` on the input so a theme can paint the filled portion of the track.
|
|
45
|
+
|
|
46
|
+
The `bar` variant sets `--sp-progress-percent` too, on the controller element, alongside the fill target's width. A theme can use it to split the readout's color at the fill edge. It is unset until the controller connects, so read it as `var(--sp-progress-percent, 0)` to match the server-rendered (empty) fill.
|
|
47
|
+
|
|
48
|
+
The rendered number is the value clamped to `[min, max]`, so an out-of-range `current` reads as the nearest bound rather than an impossible percentage. An empty or inverted range (`max <= min`) renders `0%`. While `indeterminate`, the readout is blank and `aria-valuetext` is removed.
|
|
30
49
|
|
|
31
50
|
## Methods
|
|
32
51
|
|
|
33
|
-
| Method | Wired via
|
|
34
|
-
| ---------------------------- |
|
|
35
|
-
| `setValue(value)` | —
|
|
36
|
-
| `currentValueChanged(value)` | Stimulus value callback
|
|
52
|
+
| Method | Wired via | Purpose |
|
|
53
|
+
| ---------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
54
|
+
| `setValue(value)` | — | Programmatic API — clamps to `[min, max]`, updates `currentValue`, dispatches `progress:changed` |
|
|
55
|
+
| `currentValueChanged(value)` | Stimulus value callback | Recalculates fill/meter attrs whenever `current` changes (covers `setValue()` and direct attribute edits) |
|
|
56
|
+
| `refresh()` | `input->progress#refresh` | Range-only. Reads the native input's value and passes it to `setValue()` |
|
|
37
57
|
|
|
38
58
|
## Dispatches
|
|
39
59
|
|
|
40
|
-
| Event | Detail | When
|
|
41
|
-
| ------------------ | --------------------- |
|
|
42
|
-
| `progress:changed` | `{ value, min, max }` | After `setValue()` updates the value |
|
|
60
|
+
| Event | Detail | When |
|
|
61
|
+
| ------------------ | --------------------- | -------------------------------------------------------------------------------------- |
|
|
62
|
+
| `progress:changed` | `{ value, min, max }` | After `setValue()` updates the value — including a range drag, which routes through it |
|
|
43
63
|
|
|
44
64
|
## Example HTML
|
|
45
65
|
|
|
@@ -55,6 +75,18 @@ Value-driven progress indicator supporting four render variants: a linear bar, a
|
|
|
55
75
|
<div data-progress-target="fill"></div>
|
|
56
76
|
</div>
|
|
57
77
|
|
|
78
|
+
<!-- Bar with an on-screen readout -->
|
|
79
|
+
<div
|
|
80
|
+
role="progressbar"
|
|
81
|
+
data-controller="progress"
|
|
82
|
+
data-progress-current-value="45"
|
|
83
|
+
data-progress-max-value="100"
|
|
84
|
+
data-progress-format-value="percent"
|
|
85
|
+
>
|
|
86
|
+
<div data-progress-target="fill"></div>
|
|
87
|
+
<span data-progress-target="value" aria-hidden="true">45%</span>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
58
90
|
<!-- Segmented — one fill per segment; number of segments = number of fill targets -->
|
|
59
91
|
<div
|
|
60
92
|
role="progressbar"
|
|
@@ -92,6 +124,34 @@ Value-driven progress indicator supporting four render variants: a linear bar, a
|
|
|
92
124
|
data-progress-max-value="100"
|
|
93
125
|
></meter>
|
|
94
126
|
|
|
127
|
+
<!-- Range — no readout, so the controller sits on the input itself -->
|
|
128
|
+
<input
|
|
129
|
+
type="range"
|
|
130
|
+
min="0"
|
|
131
|
+
max="100"
|
|
132
|
+
value="45"
|
|
133
|
+
style="--sp-progress-percent: 45"
|
|
134
|
+
data-controller="progress"
|
|
135
|
+
data-progress-variant-value="range"
|
|
136
|
+
data-progress-current-value="45"
|
|
137
|
+
data-progress-min-value="0"
|
|
138
|
+
data-progress-max-value="100"
|
|
139
|
+
data-action="input->progress#refresh"
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<!-- Range with a readout — a target must be a descendant, so a wrapper hosts the controller -->
|
|
143
|
+
<div
|
|
144
|
+
data-controller="progress"
|
|
145
|
+
data-progress-variant-value="range"
|
|
146
|
+
data-progress-current-value="45"
|
|
147
|
+
data-progress-max-value="100"
|
|
148
|
+
data-progress-format-value="percent"
|
|
149
|
+
data-action="input->progress#refresh"
|
|
150
|
+
>
|
|
151
|
+
<input type="range" min="0" max="100" value="45" style="--sp-progress-percent: 45" data-progress-target="input" />
|
|
152
|
+
<span data-progress-target="value" aria-hidden="true">45%</span>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
95
155
|
<!-- Indeterminate -->
|
|
96
156
|
<div role="progressbar" data-controller="progress" data-progress-indeterminate-value="true">
|
|
97
157
|
<div data-progress-target="fill"></div>
|
data/vendor/controller/guide.md
CHANGED
|
@@ -3,14 +3,68 @@
|
|
|
3
3
|
For a non-Rails / plain JS consumer of `@stimulus-plumbers/controllers`. Rails apps get this wired
|
|
4
4
|
automatically via the `stimulus_plumbers` gem's `sp_*` helpers — skip this guide for Rails.
|
|
5
5
|
|
|
6
|
+
## Install
|
|
7
|
+
|
|
6
8
|
```bash
|
|
7
|
-
npm install @stimulus-plumbers/controllers
|
|
9
|
+
npm install @hotwired/stimulus @stimulus-plumbers/controllers
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Register
|
|
13
|
+
|
|
14
|
+
Every controller is a named export. Import the ones you use and register each under its identifier:
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
import { Application } from '@hotwired/stimulus';
|
|
18
|
+
import { PopoverController, ProgressController, ComboboxDateController } from '@stimulus-plumbers/controllers';
|
|
19
|
+
|
|
20
|
+
const application = Application.start();
|
|
21
|
+
|
|
22
|
+
application.register('popover', PopoverController);
|
|
23
|
+
application.register('progress', ProgressController);
|
|
24
|
+
application.register('combobox-date', ComboboxDateController);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The export name is the identifier in PascalCase plus `Controller` — `combobox-date` →
|
|
28
|
+
`ComboboxDateController`. For the full identifier list ask `list_controllers` / read
|
|
29
|
+
`controller://index`; each one's targets, values, classes, outlets, and events are in
|
|
30
|
+
`get_controller_schema(name: identifier)` — `name:` is the identifier (`combobox-date`), not the
|
|
31
|
+
export name. Narrative docs come from `get_controller_docs(name: family)`, which takes the family
|
|
32
|
+
(`combobox`) rather than the identifier; `list_controller_docs` lists the families. Outside MCP, the
|
|
33
|
+
[Controllers table](https://github.com/ryancyq/stimulus-plumbers/blob/main/stimulus-plumbers/README.md#controllers)
|
|
34
|
+
lists the same identifiers.
|
|
35
|
+
|
|
36
|
+
Registering a controller the page never uses is harmless — Stimulus only instantiates on a matching
|
|
37
|
+
`data-controller`.
|
|
38
|
+
|
|
39
|
+
## Wire the markup
|
|
40
|
+
|
|
41
|
+
Interactive components (combobox, popover, calendar) expect their `data-controller`, target, and
|
|
42
|
+
value attributes to already be present in the rendered HTML. Rails apps get these from `sp_*`
|
|
43
|
+
helpers; a plain-JS setup writes them by hand, following the HTML structure in each controller's
|
|
44
|
+
doc:
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<div
|
|
48
|
+
role="progressbar"
|
|
49
|
+
data-controller="progress"
|
|
50
|
+
data-progress-current-value="30"
|
|
51
|
+
data-progress-min-value="0"
|
|
52
|
+
data-progress-max-value="100"
|
|
53
|
+
>
|
|
54
|
+
<div data-progress-target="fill"></div>
|
|
55
|
+
</div>
|
|
8
56
|
```
|
|
9
57
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
58
|
+
Markup shape is per-controller — take the authoritative structure from `get_controller_docs(name:)`
|
|
59
|
+
rather than adapting this example.
|
|
60
|
+
|
|
61
|
+
## Styling
|
|
62
|
+
|
|
63
|
+
Controllers ship no CSS; they toggle classes and attributes only. Bring your own styles, or use the
|
|
64
|
+
`stimulus_plumbers_tailwind` gem's token set (`guide://tailwind`).
|
|
65
|
+
|
|
66
|
+
## Accessibility
|
|
13
67
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
68
|
+
Keyboard, focus, and ARIA behaviour per component is in `aria://reference` — read it before
|
|
69
|
+
hand-writing markup, since the controllers assume the documented roles and relationships are
|
|
70
|
+
present.
|