phlex-forms 0.2.4 → 0.2.5

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: 74bd32547d30535c35060ea6c93f9cfd199614a9ced2cf1a95a7c32fe62eb3e6
4
- data.tar.gz: cf0087d9bee7cb13366ef489fb8764c82867b406881934eb92cdb7dbc271d0f8
3
+ metadata.gz: '0038bad89aee72fb6b4e162bfc83dc9afbaadbc2280f3ea23be2985e81774d41'
4
+ data.tar.gz: 9b35b71658c4ffbb14667a79e6e85e4a0437d6953b64921272ae629a443a0ac2
5
5
  SHA512:
6
- metadata.gz: 33980ae64283b549a82ab25bd445df927616ac7bc5d0929c41aba13c10439d795c631268e6154bd64a5c0b9783baee4573e7e4efdeef7ecd3e8ac4633791b1bd
7
- data.tar.gz: d4d4efd19dd0dbde90311922d91040052925a87948cdcd95d42b50049028186a6cd1b7b80a8b7027154cadd67e8801ad242777817a1ab214d13088e40a5558b3
6
+ metadata.gz: b8ada79a4a87a79096b16f82d000ea0e5620096ebe4b8fff9560ae5e556ff982ff6e13ccb52c77e0d7ea72e0d094055595870468de9c40d9f498350bab399eb4
7
+ data.tar.gz: bce76ac9a71f33cab8c15f6f4b529b636da88d802c4b86ba07ee90948daaa1a2e1c0637f898a11be75e37ab4b8dd7abb8f42ce654a9481907b5cbcc5a299263a
data/CHANGELOG.md CHANGED
@@ -16,6 +16,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
  Shares one array-valued field name with a leading empty-array hidden field,
17
17
  derives the checked set from the model's current value, and renders under both
18
18
  themes. `variant:` (`:stack`/`:inline`/`:pill`) is layout-only, no JS.
19
+ - **`checkbox_group` accessible name** (issue #17): the `div[role="group"]` can
20
+ now carry an accessible name/description. HTML/ARIA attributes pass straight
21
+ through to the group, so the bare verb is named with plain `aria:`
22
+ (`aria: { label: "Tags" }` or `aria: { labelledby: "id" }`); through `f.field`
23
+ the Control's own visible `label:` / `hint:` get stable ids and the field wires
24
+ `aria-labelledby` / `aria-describedby` at them (no duplicate markup). Absent a
25
+ name, output is unchanged.
19
26
 
20
27
  ### Changed
21
28
 
data/README.md CHANGED
@@ -366,9 +366,11 @@ f.checkbox_group(:tag_ids, Tag.all, value: :id, label: :name)
366
366
  f.checkbox_group(:tag_ids, Tag.all, value: :id,
367
367
  label: ->(t) { t.name.presence || t.slug }, # Symbol method or Proc
368
368
  variant: :pill, # :stack (default) | :inline | :pill
369
- size: :sm) # daisyUI checkbox size
370
- # ...or through field inference:
371
- f.field :tag_ids, as: :checkbox_group, collection: Tag.all, value: :id, label: :name
369
+ size: :sm, # daisyUI checkbox size
370
+ aria: { label: "Tags" }) # names the group for screen readers
371
+ # ...or through field inference (the field's label/hint name the group):
372
+ f.field :tag_ids, as: :checkbox_group, collection: Tag.all, value: :id,
373
+ label: "Tags", hint: "Pick any"
372
374
 
373
375
  f.collection_select(:country_id, Country.all, :id, :name, prompt: "Select…")
374
376
  ```
@@ -379,6 +381,20 @@ set comes from the model's current value matched by each item's resolved
379
381
  `value:` — re-rendering an edit form pre-checks the right boxes. The `:pill`
380
382
  variant styles the active chip with Tailwind's `has-[:checked]:` (no JS).
381
383
 
384
+ A `role="group"` needs an **accessible name** for assistive tech. The verb has
385
+ no bespoke naming option — HTML/ARIA attributes pass straight through to the
386
+ group, so name it with plain `aria:` (`aria: { label: "Tags" }` for a literal
387
+ name, or `aria: { labelledby: "some_id" }` to point at an existing element).
388
+ Through `f.field`, the Control's own visible `label:` / `hint:` name the group
389
+ automatically (the field wires `aria-labelledby` / `aria-describedby` at them).
390
+ Without a name the group renders as before — naming is the caller's call, the
391
+ same posture as Rails' derived form markup.
392
+
393
+ Field ids derive from scope + name (+ value for group items), exactly like
394
+ Rails' `form_with`; the gem does not guarantee page-wide id uniqueness across
395
+ multiple forms for the same model — scope one form (`Form(model:, scope: …)`) to
396
+ disambiguate, as you would in Rails.
397
+
382
398
  `Form(model: @item, scope: false)` emits **bare** field names
383
399
  (`name="quantity"`) — the shape phlex-reactive row editors and
384
400
  `<template>`-cloned rows need. External widgets bind through the public
@@ -15,6 +15,18 @@ module Forms
15
15
  # variant: :stack (default) | :inline | :pill — layout only, zero JS
16
16
  # size: daisyUI checkbox size modifier (:xs :sm :md :lg :xl)
17
17
  #
18
+ # Accessible name (issue #17): `role="group"` needs one so a screen reader
19
+ # announces the group when focus enters a checkbox. The leaf does NOT invent
20
+ # its own naming API — extra attributes pass straight through to the group
21
+ # `div`, so the caller names it with plain HTML/ARIA:
22
+ #
23
+ # f.checkbox_group(:tag_ids, Tag.all, value: :id, aria: { label: "Tags" })
24
+ # f.checkbox_group(:tag_ids, Tag.all, value: :id, aria: { labelledby: "hdr" })
25
+ #
26
+ # Through `f.field`, the builder points the group at the Control's own visible
27
+ # <label>/hint via `aria: { labelledby:, describedby: }` (so the accessible
28
+ # name matches what sighted users see) — same passthrough, no special API.
29
+ #
18
30
  # The checked set is passed in pre-resolved by the builder (Field#checkbox_group
19
31
  # matches the model's current value by each item's resolved value:), so the
20
32
  # component itself stays presentation-only. Each checkbox's markup is delegated
@@ -44,7 +56,11 @@ module Forms
44
56
  # convention as collection_check_boxes).
45
57
  input(type: "hidden", name: @name, value: "")
46
58
 
47
- div(class: group_classes, role: "group", "aria-invalid": @error || nil) do
59
+ # class is the per-checkbox styling seam (see render_checkbox), not a group
60
+ # attribute — everything else the caller passed lands on the group so aria:,
61
+ # data:, id: etc. pass straight through.
62
+ div(class: group_classes, role: "group", "aria-invalid": @error || nil,
63
+ **@attributes.except(:class)) do
48
64
  @options.each { |option| item(option) }
49
65
  end
50
66
  end
@@ -12,7 +12,7 @@ module Forms
12
12
  def view_template
13
13
  return unless @text
14
14
 
15
- p(class: classes) { @text }
15
+ p(class: classes, **@options.except(:class)) { @text }
16
16
  end
17
17
 
18
18
  private
@@ -6,27 +6,33 @@ module Forms
6
6
  # workhorse behind the Control-first `f.field` API and the explicit `f.Control`
7
7
  # escape hatch.
8
8
  class FormControl < Phlex::HTML
9
- def initialize(*modifiers, label: nil, hint: nil, error: nil, for: nil, required: false, **options)
9
+ # label_id:/hint_id: give the label and hint stable ids so a group control
10
+ # (checkbox_group's div[role="group"], which a plain `for`/`id` can't name)
11
+ # can reference them via aria-labelledby / aria-describedby (issue #17).
12
+ def initialize(*modifiers, label: nil, hint: nil, error: nil, for: nil, required: false,
13
+ label_id: nil, hint_id: nil, **options)
10
14
  @modifiers = modifiers
11
15
  @label = label
12
16
  @hint = hint
13
17
  @error = error
14
18
  @field_id = grab(for:)
15
19
  @required = required
20
+ @label_id = label_id
21
+ @hint_id = hint_id
16
22
  @options = options
17
23
  super()
18
24
  end
19
25
 
20
26
  def view_template(&)
21
27
  div(class: control_classes, **@options.except(:class)) do
22
- render Forms::Label.new(text: @label, for: @field_id, required: @required) if @label
28
+ render Forms::Label.new(text: @label, for: @field_id, required: @required, id: @label_id) if @label
23
29
 
24
30
  yield if block_given?
25
31
 
26
32
  if @error
27
33
  render Forms::FieldError.new(message: @error)
28
34
  elsif @hint
29
- render Forms::FieldHint.new(text: @hint)
35
+ render Forms::FieldHint.new(text: @hint, id: @hint_id)
30
36
  end
31
37
  end
32
38
  end
@@ -7,14 +7,14 @@ module Forms
7
7
  class Control < Forms::FormControl
8
8
  def view_template
9
9
  div(**@options.except(:class), class: @options[:class]) do
10
- render Label.new(text: @label, for: @field_id, required: @required) if @label
10
+ render Label.new(text: @label, for: @field_id, required: @required, id: @label_id) if @label
11
11
 
12
12
  yield if block_given?
13
13
 
14
14
  if @error
15
15
  render FieldError.new(message: @error)
16
16
  elsif @hint
17
- render FieldHint.new(text: @hint)
17
+ render FieldHint.new(text: @hint, id: @hint_id)
18
18
  end
19
19
  end
20
20
  end
@@ -81,11 +81,39 @@ module PhlexForms
81
81
  options = fo.apply_validations(options)
82
82
  choices ||= materialize_choices(inferred.choices)
83
83
 
84
- render fo.control(label: label_text, hint:, required: req) do
84
+ # A checkbox_group renders div[role="group"], which a plain <label for> can't
85
+ # name. Give the Control's label/hint stable ids (control_opts) and point the
86
+ # group at them with a passed-through aria: { labelledby:, describedby: }
87
+ # (group_opts), reusing the Control's own visible chrome (issue #17).
88
+ control_opts, group_opts = group_aria(fo, inferred.as, label_text, hint)
89
+ options = options.merge(group_opts)
90
+
91
+ render fo.control(label: label_text, hint:, required: req, **control_opts) do
85
92
  render_field_input(fo, inferred.name, inferred.as, modifiers, choices:, required: req, **options)
86
93
  end
87
94
  end
88
95
 
96
+ # For a checkbox_group field: the stable ids to stamp on the Control's
97
+ # label/hint (control_opts), and a group `aria:` hash pointing back at them
98
+ # (group_opts) — passed through to the group div, no special leaf API. Returns
99
+ # [{}, {}] for every other field type (a label associates via for/id, no aria
100
+ # needed) and when neither label nor hint is present.
101
+ def group_aria(fo, as, label_text, hint)
102
+ return [{}, {}] unless as == :checkbox_group
103
+
104
+ control_opts = {}
105
+ aria = {}
106
+ if label_text
107
+ control_opts[:label_id] = "#{fo.field_id}_label"
108
+ aria[:labelledby] = control_opts[:label_id]
109
+ end
110
+ if hint
111
+ control_opts[:hint_id] = "#{fo.field_id}_hint"
112
+ aria[:describedby] = control_opts[:hint_id]
113
+ end
114
+ [control_opts, aria.empty? ? {} : { aria: }]
115
+ end
116
+
89
117
  # ------------------------------------------------------------------
90
118
  # Layout helpers
91
119
  # ------------------------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhlexForms
4
- VERSION = "0.2.4"
4
+ VERSION = "0.2.5"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-forms
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson