phlex_kit 0.11.0 → 0.12.0

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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/app/components/phlex_kit/alert_dialog/alert_dialog.css +3 -1
  3. data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +13 -2
  4. data/app/components/phlex_kit/alert_dialog/alert_dialog_controller.js +35 -7
  5. data/app/components/phlex_kit/attachment/attachment.css +15 -0
  6. data/app/components/phlex_kit/attachment/attachment_trigger.rb +3 -0
  7. data/app/components/phlex_kit/breadcrumb/breadcrumb.css +1 -0
  8. data/app/components/phlex_kit/calendar/calendar_controller.js +23 -6
  9. data/app/components/phlex_kit/carousel/carousel_controller.js +36 -2
  10. data/app/components/phlex_kit/chart/chart.rb +6 -4
  11. data/app/components/phlex_kit/chart/chart_controller.js +37 -8
  12. data/app/components/phlex_kit/codeblock/codeblock.rb +5 -1
  13. data/app/components/phlex_kit/combobox/combobox.css +1 -1
  14. data/app/components/phlex_kit/combobox/combobox_controller.js +57 -2
  15. data/app/components/phlex_kit/command/command_controller.js +17 -1
  16. data/app/components/phlex_kit/command/command_input.rb +3 -2
  17. data/app/components/phlex_kit/context_menu/context_menu.css +3 -0
  18. data/app/components/phlex_kit/context_menu/context_menu.rb +9 -1
  19. data/app/components/phlex_kit/context_menu/context_menu_controller.js +49 -12
  20. data/app/components/phlex_kit/data_table/data_table.css +1 -1
  21. data/app/components/phlex_kit/data_table/data_table_pagination.rb +4 -1
  22. data/app/components/phlex_kit/data_table/data_table_per_page_select.rb +8 -3
  23. data/app/components/phlex_kit/data_table/data_table_search.rb +7 -2
  24. data/app/components/phlex_kit/date_picker/date_picker.rb +13 -2
  25. data/app/components/phlex_kit/date_picker/date_picker_controller.js +28 -0
  26. data/app/components/phlex_kit/dialog/dialog_controller.js +3 -0
  27. data/app/components/phlex_kit/drawer/drawer.rb +1 -1
  28. data/app/components/phlex_kit/drawer/drawer_content.rb +10 -3
  29. data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +4 -0
  30. data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +3 -1
  31. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_controller.js +54 -13
  32. data/app/components/phlex_kit/form_field/form_field_controller.js +32 -24
  33. data/app/components/phlex_kit/hover_card/hover_card.css +6 -0
  34. data/app/components/phlex_kit/hover_card/hover_card_controller.js +24 -0
  35. data/app/components/phlex_kit/input/input.css +0 -1
  36. data/app/components/phlex_kit/input_otp/input_otp.rb +6 -3
  37. data/app/components/phlex_kit/input_otp/input_otp_controller.js +24 -2
  38. data/app/components/phlex_kit/item/item.css +2 -1
  39. data/app/components/phlex_kit/kbd/kbd.css +2 -1
  40. data/app/components/phlex_kit/label/label.css +1 -1
  41. data/app/components/phlex_kit/link/link.rb +1 -0
  42. data/app/components/phlex_kit/marker/marker.css +2 -1
  43. data/app/components/phlex_kit/menubar/menubar.css +3 -0
  44. data/app/components/phlex_kit/menubar/menubar_controller.js +40 -16
  45. data/app/components/phlex_kit/native_select/native_select.css +14 -0
  46. data/app/components/phlex_kit/navigation_menu/navigation_menu_link.rb +8 -1
  47. data/app/components/phlex_kit/popover/popover_controller.js +19 -0
  48. data/app/components/phlex_kit/scroll_area/scroll_area.rb +1 -14
  49. data/app/components/phlex_kit/select/select_controller.js +22 -0
  50. data/app/components/phlex_kit/select/select_trigger.rb +4 -1
  51. data/app/components/phlex_kit/sheet/sheet_content.rb +13 -3
  52. data/app/components/phlex_kit/sheet/sheet_content_controller.js +36 -0
  53. data/app/components/phlex_kit/sheet/sheet_controller.js +17 -2
  54. data/app/components/phlex_kit/spinner/spinner.rb +6 -3
  55. data/app/components/phlex_kit/switch/switch.rb +6 -0
  56. data/app/components/phlex_kit/tabs/tabs_controller.js +8 -2
  57. data/app/components/phlex_kit/toast/toast_action.rb +1 -1
  58. data/app/components/phlex_kit/toast/toast_close.rb +6 -3
  59. data/app/components/phlex_kit/toast/toast_controller.js +32 -16
  60. data/app/components/phlex_kit/toast/toast_item.rb +11 -7
  61. data/app/components/phlex_kit/toast/toast_region.rb +12 -3
  62. data/app/components/phlex_kit/toast/toaster_controller.js +2 -0
  63. data/app/components/phlex_kit/toggle_group/toggle_group.rb +16 -1
  64. data/app/components/phlex_kit/toggle_group/toggle_group_controller.js +11 -8
  65. data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +15 -5
  66. data/app/javascript/phlex_kit/controllers/index.js +2 -0
  67. data/lib/phlex_kit/base_component.rb +13 -3
  68. data/lib/phlex_kit/version.rb +1 -1
  69. data/lib/phlex_kit.rb +1 -0
  70. metadata +2 -1
@@ -10,6 +10,12 @@ module PhlexKit
10
10
  class ToastRegion < BaseComponent
11
11
  SKELETON_VARIANTS = %i[default success error warning info loading].freeze
12
12
 
13
+ # Sentinel default for dir: — distinguishes "caller didn't pass dir:" (omit
14
+ # the attribute, inherit from the page) from an explicit dir: :ltr (must
15
+ # render dir="ltr"), which a plain `:ltr` default can't tell apart.
16
+ DIR_UNSET = Object.new.freeze
17
+ private_constant :DIR_UNSET
18
+
13
19
  POSITIONS = {
14
20
  top_left: "top-left",
15
21
  top_center: "top-center",
@@ -30,7 +36,7 @@ module PhlexKit
30
36
  rich_colors: false,
31
37
  close_button: false,
32
38
  hotkey: %w[alt t],
33
- dir: :ltr,
39
+ dir: DIR_UNSET,
34
40
  flash: nil,
35
41
  id: "pk-toaster",
36
42
  **attrs
@@ -53,7 +59,8 @@ module PhlexKit
53
59
  end
54
60
  @close_button = close_button
55
61
  @hotkey = hotkey
56
- @dir = dir.to_sym
62
+ @dir_explicit = !dir.equal?(DIR_UNSET)
63
+ @dir = (@dir_explicit ? dir : :ltr).to_sym
57
64
  unless %i[ltr rtl auto].include?(@dir)
58
65
  raise ArgumentError, "ToastRegion dir: must be :ltr, :rtl or :auto"
59
66
  end
@@ -138,7 +145,9 @@ module PhlexKit
138
145
  phlex_kit__toaster_hotkey_value: Array(@hotkey).join("+")
139
146
  }
140
147
  }
141
- base[:dir] = @dir.to_s unless @dir == :ltr
148
+ # Only an explicit dir: renders the attribute — the true default (no
149
+ # dir: passed) omits it so the toast inherits the page's direction.
150
+ base[:dir] = @dir.to_s if @dir_explicit
142
151
  base
143
152
  end
144
153
  end
@@ -172,6 +172,7 @@ export default class extends Controller {
172
172
  )
173
173
  return
174
174
  }
175
+ if (!this._listEl) return
175
176
  const el = this._listEl.querySelector(`#${CSS.escape(id)}`)
176
177
  if (el) el.dispatchEvent(new CustomEvent("phlex-kit:toast:force-dismiss", { bubbles: true }))
177
178
  }
@@ -329,6 +330,7 @@ export default class extends Controller {
329
330
  }
330
331
 
331
332
  _mutate(id, variant, text) {
333
+ if (!this._listEl) return
332
334
  const el = this._listEl.querySelector(`#${CSS.escape(id)}`)
333
335
  if (!el) return
334
336
  el.dataset.variant = variant
@@ -28,13 +28,28 @@ module PhlexKit
28
28
 
29
29
  def item_context
30
30
  { type: @type, variant: @variant, size: @size, disabled: @disabled,
31
- selected_values: selected_values, spacing: @spacing, orientation: @orientation }
31
+ selected_values: selected_values, spacing: @spacing, orientation: @orientation,
32
+ group: self }
32
33
  end
33
34
 
34
35
  def ToggleGroupItem(**kwargs, &block)
35
36
  render PhlexKit::ToggleGroupItem.new(group_context: item_context, **kwargs), &block
36
37
  end
37
38
 
39
+ # Roving-tabindex initial stop (single-type, nothing selected): the FIRST
40
+ # enabled item claims tabindex="0" server-side, mirroring what
41
+ # toggle_group_controller.js's reconcile() computes at runtime
42
+ # (`pressed || enabledItems()[0]`). Items call this once each, in render
43
+ # order, as they're constructed — a single-threaded sequential claim, not
44
+ # a lookahead over siblings not yet rendered.
45
+ def claim_tab_stop(item_disabled)
46
+ return false unless selected_values.empty?
47
+ return false if item_disabled
48
+ return false if @tab_stop_claimed
49
+ @tab_stop_claimed = true
50
+ true
51
+ end
52
+
38
53
  private
39
54
 
40
55
  def selected_values
@@ -33,14 +33,17 @@ export default class extends Controller {
33
33
  if (items.length === 0) return
34
34
  const currentIndex = items.indexOf(event.currentTarget)
35
35
  let nextIndex = currentIndex
36
- switch (event.key) {
37
- case "ArrowRight": case "ArrowDown": nextIndex = (currentIndex + 1) % items.length; break
38
- case "ArrowLeft": case "ArrowUp": nextIndex = (currentIndex - 1 + items.length) % items.length; break
39
- case "Home": nextIndex = 0; break
40
- case "End": nextIndex = items.length - 1; break
41
- case " ": case "Enter": event.preventDefault(); event.currentTarget.click(); return
42
- default: return
43
- }
36
+ // In RTL the horizontal arrows mirror (physical LEFT = next); Up/Down are
37
+ // unaffected. Runtime dir check is reliable after a dynamic flip.
38
+ const rtl = getComputedStyle(this.element).direction === "rtl"
39
+ const fwd = rtl ? "ArrowLeft" : "ArrowRight"
40
+ const back = rtl ? "ArrowRight" : "ArrowLeft"
41
+ if (event.key === fwd || event.key === "ArrowDown") nextIndex = (currentIndex + 1) % items.length
42
+ else if (event.key === back || event.key === "ArrowUp") nextIndex = (currentIndex - 1 + items.length) % items.length
43
+ else if (event.key === "Home") nextIndex = 0
44
+ else if (event.key === "End") nextIndex = items.length - 1
45
+ else if (event.key === " " || event.key === "Enter") { event.preventDefault(); event.currentTarget.click(); return }
46
+ else return
44
47
  event.preventDefault()
45
48
  const next = items[nextIndex]
46
49
  this.updateRovingTabindex(next)
@@ -1,19 +1,27 @@
1
1
  module PhlexKit
2
2
  class ToggleGroupItem < Toggle
3
3
  def initialize(value:, group_context:, variant: nil, size: nil, **attrs)
4
- # Toggle kwargs this item renders no markup for inheriting them
5
- # silently discarded the caller's input.
6
- unsupported = attrs.keys & %i[wrapper name unpressed_value]
4
+ # Toggle kwargs this item renders no markup for, or that the group
5
+ # derives itself (pressed: comes from the group's selected_values) —
6
+ # inheriting them silently discarded the caller's input.
7
+ unsupported = attrs.keys & %i[wrapper name unpressed_value pressed]
7
8
  if unsupported.any?
8
9
  raise ArgumentError, "ToggleGroupItem does not support #{unsupported.join(", ")} (group items render a bare button)"
9
10
  end
10
11
  @item_value = value.to_s
11
12
  @group_context = group_context
12
13
  pressed = group_context[:selected_values].include?(@item_value)
14
+ item_disabled = attrs.key?(:disabled) ? attrs.delete(:disabled) : group_context[:disabled]
15
+ # Roving tabindex initial stop: when nothing is selected yet, the first
16
+ # enabled item claims tabindex="0" (see ToggleGroup#claim_tab_stop).
17
+ # group_context[:group] is nil for a hand-built context (bypassing
18
+ # ToggleGroup#item_context) — treat that as "no tab-stop claim" rather
19
+ # than raising, since standalone tests construct contexts by hand.
20
+ @is_first_tab_stop = group_context[:group]&.claim_tab_stop(item_disabled) || false
13
21
  super(pressed: pressed, name: nil, value: @item_value,
14
22
  variant: variant || group_context[:variant],
15
23
  size: size || group_context[:size],
16
- disabled: group_context[:disabled], **attrs)
24
+ disabled: item_disabled, **attrs)
17
25
  end
18
26
 
19
27
  def view_template(&block)
@@ -34,7 +42,9 @@ module PhlexKit
34
42
  a[:aria] = { checked: @pressed.to_s }
35
43
  # A disabled button can't take focus — giving the tab stop to a
36
44
  # pressed-but-disabled item made the whole group Tab-unreachable.
37
- a[:tabindex] = (@pressed && !@disabled) ? "0" : "-1"
45
+ # When nothing is selected, the first enabled item claims the initial
46
+ # roving-tabindex stop instead of every item defaulting to -1.
47
+ a[:tabindex] = (@pressed && !@disabled) || @is_first_tab_stop ? "0" : "-1"
38
48
  else
39
49
  a[:aria] = { pressed: @pressed.to_s }
40
50
  a[:tabindex] = "0"
@@ -24,6 +24,7 @@ import ContextMenuController from "phlex_kit/controllers/context_menu_controller
24
24
  import DataTableController from "phlex_kit/controllers/data_table_controller"
25
25
  import DataTableColumnVisibilityController from "phlex_kit/controllers/data_table_column_visibility_controller"
26
26
  import DataTableSearchController from "phlex_kit/controllers/data_table_search_controller"
27
+ import DatePickerController from "phlex_kit/controllers/date_picker_controller"
27
28
  import DialogController from "phlex_kit/controllers/dialog_controller"
28
29
  import DropdownMenuController from "phlex_kit/controllers/dropdown_menu_controller"
29
30
  import FormFieldController from "phlex_kit/controllers/form_field_controller"
@@ -65,6 +66,7 @@ export function registerPhlexKitControllers(application) {
65
66
  application.register("phlex-kit--data-table", DataTableController)
66
67
  application.register("phlex-kit--data-table-column-visibility", DataTableColumnVisibilityController)
67
68
  application.register("phlex-kit--data-table-search", DataTableSearchController)
69
+ application.register("phlex-kit--date-picker", DatePickerController)
68
70
  application.register("phlex-kit--dialog", DialogController)
69
71
  application.register("phlex-kit--dropdown-menu", DropdownMenuController)
70
72
  application.register("phlex-kit--form-field", FormFieldController)
@@ -36,12 +36,22 @@ module PhlexKit
36
36
  # generated default label must skip it then: `mix` merges duplicate string
37
37
  # attrs ("pagination Résultats") instead of overriding.
38
38
  def aria_labelled?
39
+ aria_key_set?(:label) || aria_key_set?(:labelledby)
40
+ end
41
+
42
+ # True when the caller already supplied their own value for a given aria
43
+ # attribute — via the aria: hash (aria: { <key>: ... }) or the flat
44
+ # "aria-<key>"/aria_<key> spelling. A component with a generated default
45
+ # for that attribute must skip it then: `mix` merges duplicate attrs
46
+ # (aria-modal="true false") instead of overriding.
47
+ def aria_key_set?(key)
39
48
  aria = @attrs[:aria] || @attrs["aria"]
40
49
  if aria.is_a?(Hash)
41
- return true if %i[label labelledby].any? { |k| aria[k] || aria[k.to_s] }
50
+ return true if aria.key?(key) || aria.key?(key.to_s)
42
51
  end
43
- [ :aria_label, "aria_label", "aria-label",
44
- :aria_labelledby, "aria_labelledby", "aria-labelledby" ].any? { |k| @attrs[k] }
52
+ dashed = "aria-#{key}"
53
+ underscored = "aria_#{key}"
54
+ [ dashed.to_sym, dashed, underscored.to_sym, underscored ].any? { |k| @attrs[k] }
45
55
  end
46
56
  end
47
57
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhlexKit
4
- VERSION = "0.11.0"
4
+ VERSION = "0.12.0"
5
5
  end
data/lib/phlex_kit.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "json" # Carousel serializes its options value standalone (no Rails to_json)
4
4
  require "securerandom" # DatePicker generates a default input id
5
5
  require "cgi" # DataTable sort/pagination build query strings standalone
6
+ require "date" # Phlex 2.4's attribute type-dispatch references Date on every render
6
7
  # phlex-rails references ActiveSupport::SafeBuffer at require time; in a Rails
7
8
  # app it's already loaded, but a bare `require "phlex_kit"` (script/console)
8
9
  # crashes with "uninitialized constant Phlex::ActiveSupport" without these.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Kennedy
@@ -306,6 +306,7 @@ files:
306
306
  - app/components/phlex_kit/data_table/data_table_toolbar.rb
307
307
  - app/components/phlex_kit/date_picker/date_picker.css
308
308
  - app/components/phlex_kit/date_picker/date_picker.rb
309
+ - app/components/phlex_kit/date_picker/date_picker_controller.js
309
310
  - app/components/phlex_kit/dialog/dialog.css
310
311
  - app/components/phlex_kit/dialog/dialog.rb
311
312
  - app/components/phlex_kit/dialog/dialog_close.rb