stimulus_plumbers 0.4.5 → 0.4.9
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 +23 -0
- data/README.md +15 -1
- data/app/assets/javascripts/stimulus-plumbers/controllers.manifest.json +331 -20
- data/app/assets/javascripts/stimulus-plumbers/index.es.js +493 -226
- 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/app/assets/stylesheets/stimulus_plumbers/tokens.css +58 -53
- data/docs/architecture.md +0 -2
- data/docs/component/avatar.md +1 -0
- data/docs/component/button.md +2 -1
- data/docs/component/calendar.md +18 -20
- data/docs/component/card.md +14 -12
- data/docs/component/checklist.md +153 -0
- data/docs/component/combobox.md +2 -2
- data/docs/component/form.md +32 -6
- data/docs/component/icon.md +7 -5
- data/docs/component/indicator.md +55 -0
- data/docs/component/list.md +4 -5
- data/docs/component/ordered_list.md +118 -0
- data/docs/component/plumber.md +43 -0
- data/docs/component/popover.md +1 -2
- data/docs/component/progress.md +65 -0
- data/docs/component/theme.md +15 -2
- data/docs/component/timeline.md +49 -14
- data/docs/guide.md +39 -0
- data/lib/generators/stimulus_plumbers/install/install_generator.rb +33 -0
- data/lib/stimulus_plumbers/components/button.rb +2 -1
- data/lib/stimulus_plumbers/components/card.rb +1 -1
- data/lib/stimulus_plumbers/components/checklist/item/slots.rb +13 -0
- data/lib/stimulus_plumbers/components/checklist/item.rb +64 -0
- data/lib/stimulus_plumbers/components/checklist.rb +50 -0
- data/lib/stimulus_plumbers/components/combobox/typeahead.rb +1 -0
- data/lib/stimulus_plumbers/components/combobox.rb +1 -1
- data/lib/stimulus_plumbers/components/icon.rb +2 -2
- data/lib/stimulus_plumbers/components/indicator.rb +30 -0
- data/lib/stimulus_plumbers/components/link.rb +2 -1
- data/lib/stimulus_plumbers/components/list/item.rb +1 -1
- data/lib/stimulus_plumbers/components/ordered_list/item/slots.rb +13 -0
- data/lib/stimulus_plumbers/components/ordered_list/item.rb +129 -0
- data/lib/stimulus_plumbers/components/ordered_list.rb +24 -0
- data/lib/stimulus_plumbers/components/progress_bar.rb +48 -0
- data/lib/stimulus_plumbers/components/progress_meter.rb +37 -0
- data/lib/stimulus_plumbers/components/progress_ring.rb +40 -0
- data/lib/stimulus_plumbers/components/timeline/event.rb +3 -2
- data/lib/stimulus_plumbers/components/timeline/group.rb +5 -3
- data/lib/stimulus_plumbers/engine.rb +4 -0
- data/lib/stimulus_plumbers/form/fields/inputs/password.rb +1 -0
- data/lib/stimulus_plumbers/form/fields/inputs/search.rb +1 -0
- data/lib/stimulus_plumbers/generators/css_entrypoint.rb +80 -0
- data/lib/stimulus_plumbers/generators/tokens_directive.rb +24 -0
- data/lib/stimulus_plumbers/helpers/avatar_helper.rb +1 -7
- data/lib/stimulus_plumbers/helpers/button_helper.rb +1 -7
- data/lib/stimulus_plumbers/helpers/calendar_helper.rb +2 -9
- data/lib/stimulus_plumbers/helpers/calendar_turbo_helper.rb +3 -6
- data/lib/stimulus_plumbers/helpers/checklist_helper.rb +11 -0
- data/lib/stimulus_plumbers/helpers/indicator_helper.rb +11 -0
- data/lib/stimulus_plumbers/helpers/ordered_list_helper.rb +11 -0
- data/lib/stimulus_plumbers/helpers/popover_helper.rb +1 -7
- data/lib/stimulus_plumbers/helpers/progress_helper.rb +21 -0
- data/lib/stimulus_plumbers/helpers.rb +8 -0
- data/lib/stimulus_plumbers/plumber/slots.rb +10 -3
- data/lib/stimulus_plumbers/themes/base.rb +4 -0
- data/lib/stimulus_plumbers/themes/schema/icon.rb +21 -4
- data/lib/stimulus_plumbers/themes/schema/indicator/ranges.rb +14 -0
- data/lib/stimulus_plumbers/themes/schema.rb +39 -3
- data/lib/stimulus_plumbers/version.rb +1 -1
- data/lib/stimulus_plumbers.rb +10 -0
- data/lib/tasks/stimulus_plumbers.rake +18 -0
- data/vendor/ARIA.md +108 -0
- data/vendor/component/manifest.json +212 -0
- data/vendor/controller/docs/calendar.md +212 -0
- data/vendor/controller/docs/checklist.md +45 -0
- data/vendor/controller/docs/clipboard.md +61 -0
- data/vendor/controller/docs/combobox.md +212 -0
- data/vendor/controller/docs/dismisser.md +39 -0
- data/vendor/controller/docs/flipper.md +46 -0
- data/vendor/controller/docs/input-clearable.md +43 -0
- data/vendor/controller/docs/input-formatter.md +89 -0
- data/vendor/controller/docs/modal.md +57 -0
- data/vendor/controller/docs/panner.md +24 -0
- data/vendor/controller/docs/popover.md +111 -0
- data/vendor/controller/docs/progress.md +82 -0
- data/vendor/controller/docs/reorderable.md +82 -0
- data/vendor/controller/docs/timeline.md +84 -0
- data/vendor/controller/docs/visibility.md +31 -0
- data/vendor/controller/guide.md +16 -0
- data/vendor/{controllers.manifest.json → controller/manifest.json} +331 -20
- metadata +44 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Helpers
|
|
5
|
+
module ProgressHelper
|
|
6
|
+
def sp_progress_bar(value:, min: 0, max: 100, indeterminate: false, **kwargs)
|
|
7
|
+
Components::ProgressBar.new(self).render(value: value, min: min, max: max, indeterminate: indeterminate, **kwargs)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def sp_progress_ring(value:, min: 0, max: 100, indeterminate: false, **kwargs)
|
|
11
|
+
Components::ProgressRing.new(self).render(value: value, min: min, max: max, indeterminate: indeterminate, **kwargs)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def sp_progress_meter(value:, min: 0, max: 100, low: nil, high: nil, optimum: nil, **kwargs)
|
|
15
|
+
Components::ProgressMeter.new(self).render(
|
|
16
|
+
value: value, min: min, max: max, low: low, high: high, optimum: optimum, **kwargs
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "helpers/plumber_helper"
|
|
4
4
|
require_relative "helpers/icon_helper"
|
|
5
|
+
require_relative "helpers/indicator_helper"
|
|
5
6
|
require_relative "helpers/list_helper"
|
|
7
|
+
require_relative "helpers/checklist_helper"
|
|
8
|
+
require_relative "helpers/ordered_list_helper"
|
|
6
9
|
require_relative "helpers/avatar_helper"
|
|
7
10
|
require_relative "helpers/button_helper"
|
|
8
11
|
require_relative "helpers/calendar_helper"
|
|
@@ -12,13 +15,17 @@ require_relative "helpers/combobox_helper"
|
|
|
12
15
|
require_relative "helpers/divider_helper"
|
|
13
16
|
require_relative "helpers/link_helper"
|
|
14
17
|
require_relative "helpers/popover_helper"
|
|
18
|
+
require_relative "helpers/progress_helper"
|
|
15
19
|
require_relative "helpers/timeline_helper"
|
|
16
20
|
|
|
17
21
|
module StimulusPlumbers
|
|
18
22
|
module Helpers
|
|
19
23
|
include PlumberHelper
|
|
20
24
|
include IconHelper
|
|
25
|
+
include IndicatorHelper
|
|
21
26
|
include ListHelper
|
|
27
|
+
include ChecklistHelper
|
|
28
|
+
include OrderedListHelper
|
|
22
29
|
include AvatarHelper
|
|
23
30
|
include ButtonHelper
|
|
24
31
|
include CalendarHelper
|
|
@@ -28,6 +35,7 @@ module StimulusPlumbers
|
|
|
28
35
|
include DividerHelper
|
|
29
36
|
include LinkHelper
|
|
30
37
|
include PopoverHelper
|
|
38
|
+
include ProgressHelper
|
|
31
39
|
include TimelineHelper
|
|
32
40
|
end
|
|
33
41
|
end
|
|
@@ -11,8 +11,9 @@ module StimulusPlumbers
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def initialize
|
|
14
|
+
def initialize(template = nil)
|
|
15
15
|
@slots = {}
|
|
16
|
+
@template = template
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def resolve(name)
|
|
@@ -20,7 +21,7 @@ module StimulusPlumbers
|
|
|
20
21
|
return unless entry
|
|
21
22
|
|
|
22
23
|
value = entry[:value]
|
|
23
|
-
value = value
|
|
24
|
+
value = capture_block(value) if value.is_a?(Proc)
|
|
24
25
|
block_given? ? yield(value) : value
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -38,6 +39,12 @@ module StimulusPlumbers
|
|
|
38
39
|
|
|
39
40
|
private
|
|
40
41
|
|
|
42
|
+
# Runs the slot block through the view so its ERB output is returned,
|
|
43
|
+
# not written to whatever buffer happens to be active.
|
|
44
|
+
def capture_block(block)
|
|
45
|
+
@template ? @template.capture(&block) : block.call
|
|
46
|
+
end
|
|
47
|
+
|
|
41
48
|
def set_slot(name, value, options = {})
|
|
42
49
|
@slots[name] = { value: value, options: options }
|
|
43
50
|
end
|
|
@@ -54,7 +61,7 @@ module StimulusPlumbers
|
|
|
54
61
|
|
|
55
62
|
def define_by_slot(name, by)
|
|
56
63
|
define_method(:"with_#{name}") do |&block|
|
|
57
|
-
sub = by.new
|
|
64
|
+
sub = by.new(@template)
|
|
58
65
|
block&.call(sub)
|
|
59
66
|
set_slot(name, sub)
|
|
60
67
|
nil
|
|
@@ -8,6 +8,8 @@ module StimulusPlumbers
|
|
|
8
8
|
class Base
|
|
9
9
|
SCHEMA = {
|
|
10
10
|
**Schema::LIST,
|
|
11
|
+
**Schema::CHECKLIST,
|
|
12
|
+
**Schema::ORDERED_LIST,
|
|
11
13
|
**Schema::AVATAR,
|
|
12
14
|
**Schema::BUTTON,
|
|
13
15
|
**Schema::CALENDAR,
|
|
@@ -15,9 +17,11 @@ module StimulusPlumbers
|
|
|
15
17
|
**Schema::COMBOBOX,
|
|
16
18
|
**Schema::FORM,
|
|
17
19
|
**Schema::ICON,
|
|
20
|
+
**Schema::INDICATOR,
|
|
18
21
|
**Schema::INPUT_GROUP,
|
|
19
22
|
**Schema::LAYOUT,
|
|
20
23
|
**Schema::LINK,
|
|
24
|
+
**Schema::PROGRESS,
|
|
21
25
|
**Schema::TIMELINE
|
|
22
26
|
}.freeze
|
|
23
27
|
|
|
@@ -23,7 +23,7 @@ module StimulusPlumbers
|
|
|
23
23
|
|
|
24
24
|
ELEMENT_ATTRS = {
|
|
25
25
|
path: %i[d fill fill_rule clip_rule stroke_linecap stroke_linejoin opacity].freeze,
|
|
26
|
-
circle: %i[cx cy r].freeze,
|
|
26
|
+
circle: %i[cx cy r class stroke stroke_width stroke_linecap].freeze,
|
|
27
27
|
ellipse: %i[cx cy rx ry].freeze,
|
|
28
28
|
rect: %i[x y width height rx ry].freeze,
|
|
29
29
|
line: %i[x1 y1 x2 y2].freeze,
|
|
@@ -64,9 +64,26 @@ module StimulusPlumbers
|
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def resolve_element_attrs(element_data)
|
|
67
|
-
element_data.
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
element_data.filter_map do |key, value|
|
|
68
|
+
next if key == :tag
|
|
69
|
+
next unless element_attr_allowed?(element_data[:tag], key)
|
|
70
|
+
|
|
71
|
+
[element_attr_name(key), value.to_s]
|
|
72
|
+
end.to_h
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def element_attr_allowed?(tag, key)
|
|
76
|
+
ELEMENT_ATTRS.fetch(tag, []).include?(key) || data_or_aria_attr?(key)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def element_attr_name(key)
|
|
80
|
+
return key.to_s.tr("_", "-") if data_or_aria_attr?(key)
|
|
81
|
+
|
|
82
|
+
ELEMENT_ATTR_NAMES.fetch(key, key.to_s)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def data_or_aria_attr?(key)
|
|
86
|
+
key.to_s.start_with?("data_", "aria_")
|
|
70
87
|
end
|
|
71
88
|
end
|
|
72
89
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StimulusPlumbers
|
|
4
|
+
module Themes
|
|
5
|
+
module Schema
|
|
6
|
+
module Indicator
|
|
7
|
+
module Ranges
|
|
8
|
+
TYPE = %i[dot pulse badge].freeze
|
|
9
|
+
VARIANT = %i[primary secondary tertiary success destructive warning info].freeze
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -5,6 +5,7 @@ require_relative "schema/ranges"
|
|
|
5
5
|
require_relative "schema/avatar/ranges"
|
|
6
6
|
require_relative "schema/button/ranges"
|
|
7
7
|
require_relative "schema/card/ranges"
|
|
8
|
+
require_relative "schema/indicator/ranges"
|
|
8
9
|
require_relative "schema/link/ranges"
|
|
9
10
|
require_relative "schema/form/ranges"
|
|
10
11
|
require_relative "schema/form/checkbox/ranges"
|
|
@@ -27,6 +28,24 @@ module StimulusPlumbers
|
|
|
27
28
|
list_item_description: {}.freeze
|
|
28
29
|
}.freeze
|
|
29
30
|
|
|
31
|
+
CHECKLIST = {
|
|
32
|
+
checklist: {}.freeze,
|
|
33
|
+
checklist_item: {}.freeze,
|
|
34
|
+
checklist_item_input: {}.freeze,
|
|
35
|
+
checklist_item_content: {}.freeze,
|
|
36
|
+
checklist_item_title: {}.freeze,
|
|
37
|
+
checklist_item_description: {}.freeze
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
ORDERED_LIST = {
|
|
41
|
+
ordered_list: {}.freeze,
|
|
42
|
+
ordered_list_item: {}.freeze,
|
|
43
|
+
ordered_list_item_handle: {}.freeze,
|
|
44
|
+
ordered_list_item_content: {}.freeze,
|
|
45
|
+
ordered_list_item_title: {}.freeze,
|
|
46
|
+
ordered_list_item_description: {}.freeze
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
30
49
|
AVATAR = {
|
|
31
50
|
avatar: {
|
|
32
51
|
size: { default: :md, validate: Avatar::Ranges::SIZE },
|
|
@@ -169,7 +188,16 @@ module StimulusPlumbers
|
|
|
169
188
|
}.freeze
|
|
170
189
|
|
|
171
190
|
ICON = {
|
|
172
|
-
icon: {}.freeze
|
|
191
|
+
icon: { size: { default: :lg, validate: %i[sm md lg] } }.freeze
|
|
192
|
+
}.freeze
|
|
193
|
+
|
|
194
|
+
INDICATOR = {
|
|
195
|
+
indicator: {
|
|
196
|
+
type: { default: :dot, validate: Indicator::Ranges::TYPE },
|
|
197
|
+
variant: { default: :primary, validate: Indicator::Ranges::VARIANT }
|
|
198
|
+
}.freeze,
|
|
199
|
+
indicator_wrapper: {}.freeze,
|
|
200
|
+
indicator_pulse: {}.freeze
|
|
173
201
|
}.freeze
|
|
174
202
|
|
|
175
203
|
INPUT_GROUP = {
|
|
@@ -196,6 +224,13 @@ module StimulusPlumbers
|
|
|
196
224
|
popover: {}.freeze
|
|
197
225
|
}.freeze
|
|
198
226
|
|
|
227
|
+
PROGRESS = {
|
|
228
|
+
progress_bar: {}.freeze,
|
|
229
|
+
progress_bar_fill: {}.freeze,
|
|
230
|
+
progress_ring: {}.freeze,
|
|
231
|
+
progress_meter: {}.freeze
|
|
232
|
+
}.freeze
|
|
233
|
+
|
|
199
234
|
TIMELINE = {
|
|
200
235
|
timeline: {
|
|
201
236
|
orientation: { default: :vertical, validate: %i[vertical horizontal] }
|
|
@@ -216,7 +251,6 @@ module StimulusPlumbers
|
|
|
216
251
|
timeline_item_description: {}.freeze,
|
|
217
252
|
timeline_item_detail: {}.freeze,
|
|
218
253
|
timeline_item_actions: {}.freeze,
|
|
219
|
-
timeline_item_indicator_dot: {}.freeze,
|
|
220
254
|
timeline_item_connector: {}.freeze,
|
|
221
255
|
timeline_item_content: {}.freeze,
|
|
222
256
|
timeline_track_line: {}.freeze,
|
|
@@ -224,7 +258,9 @@ module StimulusPlumbers
|
|
|
224
258
|
timeline_group: {}.freeze,
|
|
225
259
|
timeline_group_section: {}.freeze,
|
|
226
260
|
timeline_group_section_date: {}.freeze,
|
|
227
|
-
timeline_group_section_list: {
|
|
261
|
+
timeline_group_section_list: {
|
|
262
|
+
orientation: { default: :vertical, validate: %i[vertical horizontal] }
|
|
263
|
+
}.freeze
|
|
228
264
|
}.freeze
|
|
229
265
|
end
|
|
230
266
|
end
|
data/lib/stimulus_plumbers.rb
CHANGED
|
@@ -17,6 +17,7 @@ require_relative "stimulus_plumbers/plumber/base"
|
|
|
17
17
|
|
|
18
18
|
# -- UI components --
|
|
19
19
|
require_relative "stimulus_plumbers/components/icon"
|
|
20
|
+
require_relative "stimulus_plumbers/components/indicator"
|
|
20
21
|
require_relative "stimulus_plumbers/components/avatar"
|
|
21
22
|
require_relative "stimulus_plumbers/components/button"
|
|
22
23
|
require_relative "stimulus_plumbers/components/button/slots"
|
|
@@ -25,10 +26,16 @@ require_relative "stimulus_plumbers/components/link"
|
|
|
25
26
|
require_relative "stimulus_plumbers/components/link/slots"
|
|
26
27
|
require_relative "stimulus_plumbers/components/card"
|
|
27
28
|
require_relative "stimulus_plumbers/components/card/slots"
|
|
29
|
+
require_relative "stimulus_plumbers/components/checklist"
|
|
30
|
+
require_relative "stimulus_plumbers/components/checklist/item"
|
|
31
|
+
require_relative "stimulus_plumbers/components/checklist/item/slots"
|
|
28
32
|
require_relative "stimulus_plumbers/components/list"
|
|
29
33
|
require_relative "stimulus_plumbers/components/list/section"
|
|
30
34
|
require_relative "stimulus_plumbers/components/list/item"
|
|
31
35
|
require_relative "stimulus_plumbers/components/list/item/slots"
|
|
36
|
+
require_relative "stimulus_plumbers/components/ordered_list"
|
|
37
|
+
require_relative "stimulus_plumbers/components/ordered_list/item"
|
|
38
|
+
require_relative "stimulus_plumbers/components/ordered_list/item/slots"
|
|
32
39
|
require_relative "stimulus_plumbers/components/timeline"
|
|
33
40
|
require_relative "stimulus_plumbers/components/timeline/event"
|
|
34
41
|
require_relative "stimulus_plumbers/components/timeline/event/slots"
|
|
@@ -38,6 +45,9 @@ require_relative "stimulus_plumbers/components/input_group"
|
|
|
38
45
|
require_relative "stimulus_plumbers/components/popover"
|
|
39
46
|
require_relative "stimulus_plumbers/components/popover/trigger"
|
|
40
47
|
require_relative "stimulus_plumbers/components/popover/panel"
|
|
48
|
+
require_relative "stimulus_plumbers/components/progress_bar"
|
|
49
|
+
require_relative "stimulus_plumbers/components/progress_ring"
|
|
50
|
+
require_relative "stimulus_plumbers/components/progress_meter"
|
|
41
51
|
|
|
42
52
|
# -- Calendar --
|
|
43
53
|
require_relative "stimulus_plumbers/components/calendar"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :stimulus_plumbers do
|
|
4
|
+
desc "Add the tokens.css @import into the CSS entry file"
|
|
5
|
+
task(:install) do
|
|
6
|
+
unless ENV["STIMULUS_PLUMBERS_SKIP_INSTALL"]
|
|
7
|
+
require "generators/stimulus_plumbers/install/install_generator"
|
|
8
|
+
StimulusPlumbers::Generators::InstallGenerator.new(
|
|
9
|
+
[], {}, { destination_root: Rails.root.to_s }
|
|
10
|
+
).invoke_all
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Rake::Task["stimulus_plumbers:install"].enhance(["environment"]) unless ENV["STIMULUS_PLUMBERS_SKIP_INSTALL"]
|
|
16
|
+
|
|
17
|
+
# Hooks into "assets:precompile" as a prerequisite; Rake merges rather than overwrites, so order-independent.
|
|
18
|
+
task "assets:precompile" => ["stimulus_plumbers:install"]
|
data/vendor/ARIA.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# WCAG 2.1 AA / ARIA Reference
|
|
2
|
+
|
|
3
|
+
## Core Criteria (all components)
|
|
4
|
+
|
|
5
|
+
| Criterion | Level | Rule |
|
|
6
|
+
|-----------|-------|------|
|
|
7
|
+
| 1.3.1 Info & Relationships | A | Convey structure via semantic HTML or ARIA roles, not visual styling alone |
|
|
8
|
+
| 1.4.1 Use of Color | A | Never use color as the only means to convey information |
|
|
9
|
+
| 1.4.3 Contrast (text) | AA | 4.5:1 normal text, 3:1 large text (18pt / 14pt bold) |
|
|
10
|
+
| 1.4.11 Non-text Contrast | AA | 3:1 for UI component boundaries and state indicators |
|
|
11
|
+
| 2.1.1 Keyboard | A | All functionality operable via keyboard; no mouse-only interactions |
|
|
12
|
+
| 2.1.2 No Keyboard Trap | A | Focus must be escapable from any component (Escape or documented key) |
|
|
13
|
+
| 2.4.3 Focus Order | A | Focus sequence must be logical and predictable |
|
|
14
|
+
| 2.4.7 Focus Visible | AA | Keyboard focus indicator must be visible |
|
|
15
|
+
| 4.1.2 Name, Role, Value | A | All UI components must have accessible name, role, and state via ARIA or native semantics; state must stay in sync dynamically (e.g. `aria-expanded`, `aria-selected`, `aria-checked`) |
|
|
16
|
+
| 4.1.3 Status Messages | AA | Status/error messages announced via `role="status"` or `aria-live` without focus move |
|
|
17
|
+
|
|
18
|
+
## JS Keyboard Navigation Patterns
|
|
19
|
+
|
|
20
|
+
Two helper classes handle keyboard navigation in controllers — see [`stimulus-plumbers/docs/accessibility/design.md`](stimulus-plumbers/docs/accessibility/design.md) for full API.
|
|
21
|
+
|
|
22
|
+
| Pattern | Class | When to use |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| Roving tabindex | `RovingTabIndex` | Disclosure widgets, trees, toolbars — focus moves between items |
|
|
25
|
+
| Managed focus / listbox | `ListboxNavigation` | Combobox listbox, `role="listbox"` — input keeps focus, `aria-selected` moves |
|
|
26
|
+
|
|
27
|
+
## Component-Specific Patterns (APG)
|
|
28
|
+
|
|
29
|
+
#### Modal (`modal_controller`)
|
|
30
|
+
- `role="dialog"`, `aria-modal="true"`, `aria-labelledby` pointing to heading
|
|
31
|
+
- Focus moves into dialog on open; returns to trigger on close
|
|
32
|
+
- Focus trapped inside — Tab/Shift+Tab cycle within; Escape closes
|
|
33
|
+
- Status announcements ("Modal opened"/"Modal closed") via `aria-live` on open/close (WCAG 4.1.3)
|
|
34
|
+
|
|
35
|
+
#### Popover (`popover_controller`)
|
|
36
|
+
- `role="dialog"` or `role="tooltip"` depending on interactivity
|
|
37
|
+
- Trigger: `<button>` with `aria-haspopup="dialog"` and `aria-expanded="false"` initially
|
|
38
|
+
- `aria-expanded` toggled to `"true"` / `"false"` by the controller via the `trigger` Stimulus target
|
|
39
|
+
- `aria-controls` linking trigger to panel id is recommended but optional
|
|
40
|
+
- Escape closes and returns focus to trigger
|
|
41
|
+
|
|
42
|
+
#### Combobox (`input_combobox_controller`, `combobox/`)
|
|
43
|
+
- Trigger: `<input role="combobox">` with `aria-haspopup` (`listbox`/`dialog`) and `aria-controls` referencing the **popup element** (the `role="listbox"`/`role="dialog"`)
|
|
44
|
+
- `role="listbox"` permits only `option`/`group` children (`aria-required-children`, WCAG 1.3.1). Status messages (loading, "No results") must be `role="status"`/`aria-live` **siblings of the listbox, never children of it** — for typeahead the listbox is nested in a wrapper panel so the status regions can sit beside it
|
|
45
|
+
- Status/loading regions inside the popover panel must stay **non-focusable** (the popover moves focus into the panel on open)
|
|
46
|
+
|
|
47
|
+
#### Calendar / Date Picker (`calendar-month`, `combobox-date`)
|
|
48
|
+
- Grid: `role="grid"`, `role="row"`, `role="gridcell"`
|
|
49
|
+
- Navigation buttons: `aria-label="Previous Month"` / `"Next Month"` (via `Combobox::Date::Navigation`)
|
|
50
|
+
- Selected date: `aria-selected="true"`; today: `aria-current="date"`
|
|
51
|
+
- Disabled dates: `aria-disabled="true"`, `tabindex="-1"`
|
|
52
|
+
- Three views: month (days grid), year (months grid), decade (years grid); zoom out via `combobox-date#zoomOut`
|
|
53
|
+
- Arrow keys navigate cells; Enter/Space select; Escape closes picker
|
|
54
|
+
|
|
55
|
+
#### Form Fields (`form/`, `form-field_controller`)
|
|
56
|
+
- Every input must have a visible `<label>` via `for`/`id` or `aria-labelledby`
|
|
57
|
+
- Required fields: `required` attribute + `aria-required="true"`
|
|
58
|
+
- Invalid fields: `aria-invalid="true"` + `aria-describedby` pointing to error message
|
|
59
|
+
- Error message element: `role="alert"` or `aria-live="polite"` so it's announced
|
|
60
|
+
|
|
61
|
+
#### Password Reveal (`input-formatter_controller`, `"password"` formatter)
|
|
62
|
+
- Toggle button: `aria-label` describes action ("Show password" / "Hide password")
|
|
63
|
+
- `aria-pressed` managed on toggle button, reflecting revealed state
|
|
64
|
+
|
|
65
|
+
#### Input Clearable (`input_clearable_controller`)
|
|
66
|
+
- Clear button: `hidden` attribute while the input is empty, removing it from the keyboard/AT tab order until there's something to clear
|
|
67
|
+
- Escape inside the input clears it (keyboard equivalent to clicking the clear button, WCAG 2.1.1); default is prevented so it doesn't also close a parent overlay
|
|
68
|
+
- Focus returns to the input after clearing (WCAG 2.4.3 Focus Order)
|
|
69
|
+
- No `aria-live` announcement — clearing is user-initiated and the button's disappearance is self-explanatory
|
|
70
|
+
|
|
71
|
+
#### Flipper / Visibility / Dismisser
|
|
72
|
+
- Trigger: `aria-expanded="true/false"` when toggling a region
|
|
73
|
+
- Controlled region: `aria-hidden="true"` when collapsed (or removed from DOM)
|
|
74
|
+
- `aria-controls` links trigger to region id
|
|
75
|
+
|
|
76
|
+
#### Button (`button/renderer`)
|
|
77
|
+
- Use `<button>` (not `<div>` / `<a>`) for actions
|
|
78
|
+
- Icon-only buttons must have `aria-label` or visually-hidden text
|
|
79
|
+
- Disabled: `disabled` attribute (not `aria-disabled` alone) unless intentionally focusable
|
|
80
|
+
|
|
81
|
+
#### List (`sp_list`, `sp_list_item`)
|
|
82
|
+
- Static list: `role="list"` + `role="listitem"` (explicitly set to preserve semantics when CSS resets strip list role)
|
|
83
|
+
- Active item: `aria-current="page"` on `<a>` links; `aria-current="true"` on `<button>` items — the value differs by element type per the ARIA spec
|
|
84
|
+
|
|
85
|
+
#### Checklist (`checklist_controller`, `sp_checklist`)
|
|
86
|
+
- Each item: native `<input type="checkbox">` inside a `<label>` — role, keyboard activation (Space), focus, and checked-state announcement are all handled by the browser. The component sets no ARIA attributes on items.
|
|
87
|
+
- Read-only item (`readonly: true`): native `disabled` attribute — removes the control from the tab order and announces it as unavailable to assistive tech. No `aria-readonly`/`tabindex` hack.
|
|
88
|
+
- Master "select all" toggle (`select_all:`): same `<input type="checkbox">` shape as an item. Its `indeterminate` property (JS-only, no HTML attribute) is set client-side by the `checklist` controller when some but not all enabled items are checked — modern browsers map `indeterminate` to the accessibility tree's `mixed` checked state automatically, satisfying WCAG 4.1.2 with no manual ARIA.
|
|
89
|
+
- 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.
|
|
90
|
+
- 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.
|
|
91
|
+
|
|
92
|
+
#### Avatar / Card / Icon
|
|
93
|
+
- Decorative images/icons: `aria-hidden="true"` or `alt=""`
|
|
94
|
+
- Meaningful images: descriptive `alt` text
|
|
95
|
+
|
|
96
|
+
#### Timeline (`timeline_controller`)
|
|
97
|
+
- List: `<ol>` (ordered) for chronological events; static timelines need no ARIA additions beyond semantic HTML
|
|
98
|
+
- Each item: `<li>`; timestamp displayed via `<time datetime="YYYY-MM-DD">`
|
|
99
|
+
- Indicators (dots, icons, avatars): decorative — always `aria-hidden="true"`
|
|
100
|
+
- Expandable items: trigger `<button>` with `aria-expanded="false/true"` + `aria-controls` → detail element id; detail has `hidden` attribute toggled by controller
|
|
101
|
+
- Trigger lives inside `<h3>` (`<h3><button aria-expanded>Title</button></h3>`) — WAI-ARIA Accordion pattern
|
|
102
|
+
- Keyboard (interactive): Up/Down arrows move focus between item triggers; Home/End jump to first/last trigger; Enter/Space toggle expansion (native button behaviour)
|
|
103
|
+
|
|
104
|
+
#### Reorderable (`reorderable_controller`)
|
|
105
|
+
- Roving tabindex on `item` targets (`RovingTabIndex`) — plain Arrow/Home/End move focus only, unaffected by editing state
|
|
106
|
+
- Keyboard move (`Alt+Arrow` on the axis matching `orientation`, default `ArrowUp`/`ArrowDown`; configurable modifier via `moveKey`; horizontal move-key meaning flips under `dir="rtl"`): moves the focused item, keeps focus on it, announces the new position via `role="status"`/`aria-live` (WCAG 4.1.3) — satisfies the keyboard-equivalent requirement (WCAG 2.1.1) for pointer drag
|
|
107
|
+
- Pointer drag: does not move focus or announce — avoids stealing focus from a mouse user who never asked for it
|
|
108
|
+
- `editingValue` gates both drag and keyboard-move; while `true`, every `trigger` target (`<a>`/`<button>` inside an item) gets `aria-disabled="true"` + `tabindex="-1"` — removes it from keyboard/AT activation and tab order without touching pointer clicks (apps/themes add their own `pointer-events: none` CSS rule for that half)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
{
|
|
2
|
+
"calendar-decade": {
|
|
3
|
+
"actions": [],
|
|
4
|
+
"listens": [
|
|
5
|
+
"selected"
|
|
6
|
+
],
|
|
7
|
+
"targets": [
|
|
8
|
+
"grid"
|
|
9
|
+
],
|
|
10
|
+
"values": []
|
|
11
|
+
},
|
|
12
|
+
"calendar-decade-selector": {
|
|
13
|
+
"actions": [],
|
|
14
|
+
"listens": [],
|
|
15
|
+
"targets": [],
|
|
16
|
+
"values": []
|
|
17
|
+
},
|
|
18
|
+
"calendar-month": {
|
|
19
|
+
"actions": [],
|
|
20
|
+
"listens": [
|
|
21
|
+
"selected"
|
|
22
|
+
],
|
|
23
|
+
"targets": [
|
|
24
|
+
"daysOfMonth",
|
|
25
|
+
"daysOfWeek"
|
|
26
|
+
],
|
|
27
|
+
"values": [
|
|
28
|
+
"month",
|
|
29
|
+
"year"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"calendar-month-selector": {
|
|
33
|
+
"actions": [],
|
|
34
|
+
"listens": [],
|
|
35
|
+
"targets": [],
|
|
36
|
+
"values": []
|
|
37
|
+
},
|
|
38
|
+
"calendar-year": {
|
|
39
|
+
"actions": [],
|
|
40
|
+
"listens": [
|
|
41
|
+
"selected"
|
|
42
|
+
],
|
|
43
|
+
"targets": [
|
|
44
|
+
"grid"
|
|
45
|
+
],
|
|
46
|
+
"values": []
|
|
47
|
+
},
|
|
48
|
+
"calendar-year-selector": {
|
|
49
|
+
"actions": [],
|
|
50
|
+
"listens": [],
|
|
51
|
+
"targets": [],
|
|
52
|
+
"values": []
|
|
53
|
+
},
|
|
54
|
+
"checklist": {
|
|
55
|
+
"actions": [
|
|
56
|
+
"onChange"
|
|
57
|
+
],
|
|
58
|
+
"listens": [],
|
|
59
|
+
"targets": [],
|
|
60
|
+
"values": []
|
|
61
|
+
},
|
|
62
|
+
"clipboard": {
|
|
63
|
+
"actions": [],
|
|
64
|
+
"listens": [],
|
|
65
|
+
"targets": [],
|
|
66
|
+
"values": []
|
|
67
|
+
},
|
|
68
|
+
"combobox-date": {
|
|
69
|
+
"actions": [],
|
|
70
|
+
"listens": [
|
|
71
|
+
"selected"
|
|
72
|
+
],
|
|
73
|
+
"targets": [],
|
|
74
|
+
"values": []
|
|
75
|
+
},
|
|
76
|
+
"combobox-dropdown": {
|
|
77
|
+
"actions": [
|
|
78
|
+
"onNavigate",
|
|
79
|
+
"select"
|
|
80
|
+
],
|
|
81
|
+
"listens": [
|
|
82
|
+
"selected"
|
|
83
|
+
],
|
|
84
|
+
"targets": [],
|
|
85
|
+
"values": []
|
|
86
|
+
},
|
|
87
|
+
"combobox-time": {
|
|
88
|
+
"actions": [
|
|
89
|
+
"onNavigate",
|
|
90
|
+
"select"
|
|
91
|
+
],
|
|
92
|
+
"listens": [
|
|
93
|
+
"selected"
|
|
94
|
+
],
|
|
95
|
+
"targets": [],
|
|
96
|
+
"values": []
|
|
97
|
+
},
|
|
98
|
+
"dismisser": {
|
|
99
|
+
"actions": [],
|
|
100
|
+
"listens": [],
|
|
101
|
+
"targets": [],
|
|
102
|
+
"values": []
|
|
103
|
+
},
|
|
104
|
+
"flipper": {
|
|
105
|
+
"actions": [],
|
|
106
|
+
"listens": [],
|
|
107
|
+
"targets": [],
|
|
108
|
+
"values": []
|
|
109
|
+
},
|
|
110
|
+
"input-clearable": {
|
|
111
|
+
"actions": [
|
|
112
|
+
"clear"
|
|
113
|
+
],
|
|
114
|
+
"listens": [],
|
|
115
|
+
"targets": [],
|
|
116
|
+
"values": []
|
|
117
|
+
},
|
|
118
|
+
"input-combobox": {
|
|
119
|
+
"actions": [
|
|
120
|
+
"onInput"
|
|
121
|
+
],
|
|
122
|
+
"listens": [
|
|
123
|
+
"changed"
|
|
124
|
+
],
|
|
125
|
+
"targets": [],
|
|
126
|
+
"values": []
|
|
127
|
+
},
|
|
128
|
+
"input-formatter": {
|
|
129
|
+
"actions": [
|
|
130
|
+
"toggle"
|
|
131
|
+
],
|
|
132
|
+
"listens": [],
|
|
133
|
+
"targets": [],
|
|
134
|
+
"values": []
|
|
135
|
+
},
|
|
136
|
+
"modal": {
|
|
137
|
+
"actions": [],
|
|
138
|
+
"listens": [],
|
|
139
|
+
"targets": [],
|
|
140
|
+
"values": []
|
|
141
|
+
},
|
|
142
|
+
"panner": {
|
|
143
|
+
"actions": [],
|
|
144
|
+
"listens": [],
|
|
145
|
+
"targets": [],
|
|
146
|
+
"values": []
|
|
147
|
+
},
|
|
148
|
+
"popover": {
|
|
149
|
+
"actions": [
|
|
150
|
+
"close",
|
|
151
|
+
"open",
|
|
152
|
+
"toggle"
|
|
153
|
+
],
|
|
154
|
+
"listens": [],
|
|
155
|
+
"targets": [],
|
|
156
|
+
"values": []
|
|
157
|
+
},
|
|
158
|
+
"progress": {
|
|
159
|
+
"actions": [],
|
|
160
|
+
"listens": [],
|
|
161
|
+
"targets": [
|
|
162
|
+
"fill",
|
|
163
|
+
"meter"
|
|
164
|
+
],
|
|
165
|
+
"values": [
|
|
166
|
+
"current",
|
|
167
|
+
"high",
|
|
168
|
+
"indeterminate",
|
|
169
|
+
"low",
|
|
170
|
+
"max",
|
|
171
|
+
"min",
|
|
172
|
+
"optimum",
|
|
173
|
+
"variant"
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
"reorderable": {
|
|
177
|
+
"actions": [
|
|
178
|
+
"onPointerDown",
|
|
179
|
+
"onPointerMove",
|
|
180
|
+
"onPointerUp"
|
|
181
|
+
],
|
|
182
|
+
"listens": [],
|
|
183
|
+
"targets": [
|
|
184
|
+
"handle",
|
|
185
|
+
"item",
|
|
186
|
+
"trigger"
|
|
187
|
+
],
|
|
188
|
+
"values": [
|
|
189
|
+
"editing",
|
|
190
|
+
"moveKey",
|
|
191
|
+
"orientation"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
"timeline": {
|
|
195
|
+
"actions": [
|
|
196
|
+
"toggle"
|
|
197
|
+
],
|
|
198
|
+
"listens": [],
|
|
199
|
+
"targets": [
|
|
200
|
+
"detail",
|
|
201
|
+
"time",
|
|
202
|
+
"trigger"
|
|
203
|
+
],
|
|
204
|
+
"values": []
|
|
205
|
+
},
|
|
206
|
+
"visibility": {
|
|
207
|
+
"actions": [],
|
|
208
|
+
"listens": [],
|
|
209
|
+
"targets": [],
|
|
210
|
+
"values": []
|
|
211
|
+
}
|
|
212
|
+
}
|