openproject-primer_view_components 0.80.2 → 0.81.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -22
  3. data/README.md +20 -1
  4. data/app/assets/javascripts/components/primer/primer.d.ts +1 -0
  5. data/app/assets/javascripts/lib/primer/forms/character_counter.d.ts +41 -0
  6. data/app/assets/javascripts/lib/primer/forms/primer_text_area.d.ts +13 -0
  7. data/app/assets/javascripts/lib/primer/forms/primer_text_field.d.ts +2 -0
  8. data/app/assets/javascripts/primer_view_components.js +1 -1
  9. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  10. data/app/assets/styles/primer_view_components.css +1 -1
  11. data/app/assets/styles/primer_view_components.css.map +1 -1
  12. data/app/components/primer/alpha/action_list/item.rb +2 -1
  13. data/app/components/primer/alpha/auto_complete/auto_complete.html.erb +8 -6
  14. data/app/components/primer/alpha/select_panel.rb +1 -1
  15. data/app/components/primer/alpha/select_panel_element.js +1 -1
  16. data/app/components/primer/alpha/select_panel_element.ts +1 -1
  17. data/app/components/primer/alpha/stack.rb +1 -0
  18. data/app/components/primer/alpha/tab_nav.css +1 -1
  19. data/app/components/primer/alpha/tab_nav.css.json +1 -0
  20. data/app/components/primer/alpha/tab_nav.css.map +1 -1
  21. data/app/components/primer/alpha/tab_nav.pcss +7 -1
  22. data/app/components/primer/alpha/text_area.rb +1 -0
  23. data/app/components/primer/alpha/text_field.rb +1 -0
  24. data/app/components/primer/alpha/toggle_switch.html.erb +2 -2
  25. data/app/components/primer/alpha/tool_tip.js +12 -5
  26. data/app/components/primer/alpha/tool_tip.ts +14 -5
  27. data/app/components/primer/beta/auto_complete/item.html.erb +5 -4
  28. data/app/components/primer/beta/avatar.rb +4 -0
  29. data/app/components/primer/beta/avatar_stack.rb +6 -0
  30. data/app/components/primer/beta/blankslate.css +1 -1
  31. data/app/components/primer/beta/blankslate.css.map +1 -1
  32. data/app/components/primer/beta/blankslate.pcss +2 -0
  33. data/app/components/primer/beta/spinner.html.erb +2 -2
  34. data/app/components/primer/primer.d.ts +1 -0
  35. data/app/components/primer/primer.js +1 -0
  36. data/app/components/primer/primer.ts +1 -0
  37. data/app/controllers/primer/view_components/toggle_switch_controller.rb +2 -2
  38. data/app/forms/check_box_with_nested_form.rb +9 -5
  39. data/app/forms/text_area_with_character_limit_form.rb +13 -0
  40. data/app/forms/text_field_with_character_limit_form.rb +13 -0
  41. data/app/lib/primer/forms/caption.html.erb +16 -9
  42. data/app/lib/primer/forms/character_counter.d.ts +41 -0
  43. data/app/lib/primer/forms/character_counter.js +114 -0
  44. data/app/lib/primer/forms/character_counter.ts +129 -0
  45. data/app/lib/primer/forms/check_box.rb +28 -0
  46. data/app/lib/primer/forms/dsl/input.rb +23 -0
  47. data/app/lib/primer/forms/dsl/multi_input.rb +3 -1
  48. data/app/lib/primer/forms/dsl/text_area_input.rb +12 -1
  49. data/app/lib/primer/forms/dsl/text_field_input.rb +11 -1
  50. data/app/lib/primer/forms/form_control.html.erb +2 -1
  51. data/app/lib/primer/forms/primer_text_area.d.ts +13 -0
  52. data/app/lib/primer/forms/primer_text_area.js +53 -0
  53. data/app/lib/primer/forms/primer_text_area.ts +37 -0
  54. data/app/lib/primer/forms/primer_text_field.d.ts +2 -0
  55. data/app/lib/primer/forms/primer_text_field.js +16 -2
  56. data/app/lib/primer/forms/primer_text_field.ts +16 -3
  57. data/app/lib/primer/forms/text_area.html.erb +6 -4
  58. data/app/lib/primer/forms/text_field.html.erb +1 -1
  59. data/app/lib/primer/forms/text_field.rb +8 -0
  60. data/lib/primer/accessibility.rb +9 -3
  61. data/lib/primer/view_components/engine.rb +1 -4
  62. data/lib/primer/view_components/version.rb +2 -2
  63. data/previews/primer/alpha/action_menu_preview/submitting_forms.html.erb +1 -1
  64. data/previews/primer/alpha/form_control_preview/playground.html.erb +4 -2
  65. data/previews/primer/alpha/octicon_symbols_preview/playground.html.erb +1 -1
  66. data/previews/primer/alpha/overlay_preview.rb +0 -25
  67. data/previews/primer/alpha/text_area_preview.rb +29 -8
  68. data/previews/primer/alpha/text_field_preview.rb +34 -4
  69. data/previews/primer/alpha/toggle_switch_preview.rb +14 -14
  70. data/previews/primer/alpha/tree_view_preview/loading_failure.html.erb +53 -1
  71. data/previews/primer/alpha/tree_view_preview/stress_test.html.erb +43 -0
  72. data/previews/primer/beta/button_preview/all_schemes.html.erb +1 -1
  73. data/previews/primer/beta/button_preview/invisible_all_visuals.html.erb +1 -1
  74. data/previews/primer/beta/button_preview/summary_as_button.html.erb +10 -1
  75. data/previews/primer/forms_preview/text_area_with_character_limit_form.html.erb +3 -0
  76. data/previews/primer/forms_preview/text_field_with_character_limit_form.html.erb +3 -0
  77. data/previews/primer/forms_preview.rb +6 -0
  78. data/static/arguments.json +13 -1
  79. data/static/form_previews.json +10 -0
  80. data/static/info_arch.json +106 -15
  81. data/static/previews.json +93 -14
  82. metadata +15 -2
@@ -302,7 +302,8 @@ module Primer
302
302
  if @list.allows_selection?
303
303
  @content_arguments[:aria] = merge_aria(
304
304
  @content_arguments,
305
- { aria: @list.aria_selection_variant == :selected ? { selected: active? } : { checked: active? } }
305
+ # Always use aria-selected for listbox (role="option" requires aria-selected per WAI-ARIA 1.2)
306
+ { aria: @list.acts_as_listbox? || @list.aria_selection_variant == :selected ? { selected: active? } : { checked: active? } }
306
307
  )
307
308
  end
308
309
 
@@ -7,16 +7,18 @@
7
7
  <% end %>
8
8
  </label>
9
9
  <span class="autocomplete-body">
10
+ <% clear_button = @is_clearable ? capture do %>
11
+ <button id="<%= @input_id %>-clear" class="btn-octicon" aria-label="Clear"><%= primer_octicon "x" %></button>
12
+ <% end : nil %>
10
13
  <% if @with_icon %>
11
14
  <div class="form-control autocomplete-embedded-icon-wrap">
12
15
  <%= render Primer::Beta::Octicon.new(:search) %>
13
- <% end %>
14
- <%= input %>
15
- <% if @is_clearable %>
16
- <button id="<%= @input_id %>-clear" class="btn-octicon" aria-label="Clear"><%= primer_octicon "x" %></button>
17
- <% end %>
18
- <% if @with_icon %>
16
+ <%= input %>
17
+ <%= clear_button %>
19
18
  </div>
19
+ <% else %>
20
+ <%= input %>
21
+ <%= clear_button %>
20
22
  <% end %>
21
23
  <%= results %>
22
24
  </span>
@@ -256,7 +256,7 @@ module Primer
256
256
  super(
257
257
  p: 2,
258
258
  role: "listbox",
259
- aria_selection_variant: select_variant == :single ? :selected : :checked,
259
+ aria_selection_variant: :selected,
260
260
  select_variant: select_variant == :multiple ? :multiple_checkbox : :single,
261
261
  **system_arguments
262
262
  )
@@ -84,7 +84,7 @@ let SelectPanelElement = class SelectPanelElement extends HTMLElement {
84
84
  return this.getAttribute('data-select-variant');
85
85
  }
86
86
  get ariaSelectionType() {
87
- return this.selectVariant === 'multiple' ? 'aria-checked' : 'aria-selected';
87
+ return 'aria-selected';
88
88
  }
89
89
  set selectVariant(variant) {
90
90
  if (variant) {
@@ -97,7 +97,7 @@ export class SelectPanelElement extends HTMLElement {
97
97
  }
98
98
 
99
99
  get ariaSelectionType(): string {
100
- return this.selectVariant === 'multiple' ? 'aria-checked' : 'aria-selected'
100
+ return 'aria-selected'
101
101
  }
102
102
 
103
103
  set selectVariant(variant: SelectVariant) {
@@ -118,6 +118,7 @@ module Primer
118
118
  DEFAULT = nil
119
119
  OPTIONS = [
120
120
  DEFAULT,
121
+ :none,
121
122
  :condensed,
122
123
  :normal,
123
124
  :spacious
@@ -1 +1 @@
1
- .tabnav{border-bottom:var(--borderWidth-thin) solid var(--borderColor-default);margin-bottom:var(--stack-gap-normal);margin-top:0}.tabnav-tabs{display:flex;margin-bottom:calc(var(--borderWidth-thin)*-1);overflow:hidden}.tabnav-tab{background-color:initial;border:var(--borderWidth-thin) solid #0000;border-bottom:0;color:var(--fgColor-muted);display:inline-block;flex-shrink:0;font-size:var(--text-body-size-medium);line-height:23px;padding:var(--base-size-8) var(--control-medium-paddingInline-spacious);-webkit-text-decoration:none;text-decoration:none;transition:color .2s cubic-bezier(.3,0,.5,1)}.tabnav-tab.selected,.tabnav-tab[aria-current]:not([aria-current=false]),.tabnav-tab[aria-selected=true]{background-color:var(--bgColor-default);border-color:var(--borderColor-default);border-radius:var(--borderRadius-medium) var(--borderRadius-medium) 0 0;color:var(--fgColor-default)}:is(.tabnav-tab.selected,.tabnav-tab[aria-selected=true],.tabnav-tab[aria-current]:not([aria-current=false])) .octicon{color:inherit}.tabnav-tab:hover{color:var(--fgColor-default);-webkit-text-decoration:none;text-decoration:none;transition-duration:.1s}.tabnav-tab:focus,.tabnav-tab:focus-visible{border-radius:var(--borderRadius-medium) var(--borderRadius-medium) 0 0!important;outline-offset:-6px}.tabnav-tab .octicon,.tabnav-tab:active{color:var(--fgColor-muted)}.tabnav-tab .octicon{margin-right:var(--control-small-gap)}.tabnav-tab .Counter{color:inherit;margin-left:var(--control-small-gap)}.tabnav-extra{color:var(--fgColor-muted);display:inline-block;font-size:var(--text-body-size-small);margin-left:10px;padding-top:10px}.tabnav-extra>.octicon{margin-right:var(--base-size-2)}a.tabnav-extra:hover{color:var(--fgColor-accent);-webkit-text-decoration:none;text-decoration:none}.tabnav-btn{margin-left:var(--controlStack-medium-gap-condensed)}
1
+ .tabnav{border-bottom:var(--borderWidth-thin) solid var(--borderColor-default);margin-bottom:var(--stack-gap-normal);margin-top:0}.tabnav-tabs{display:flex;margin-bottom:calc(var(--borderWidth-thin)*-1);overflow-x:auto;overflow-y:hidden}.tabnav-tab,.tabnav-tabs>li{flex-shrink:0}.tabnav-tab{background-color:initial;border:var(--borderWidth-thin) solid #0000;border-bottom:0;color:var(--fgColor-muted);display:inline-block;font-size:var(--text-body-size-medium);line-height:23px;padding:var(--base-size-8) var(--control-medium-paddingInline-spacious);-webkit-text-decoration:none;text-decoration:none;transition:color .2s cubic-bezier(.3,0,.5,1)}.tabnav-tab.selected,.tabnav-tab[aria-current]:not([aria-current=false]),.tabnav-tab[aria-selected=true]{background-color:var(--bgColor-default);border-color:var(--borderColor-default);border-radius:var(--borderRadius-medium) var(--borderRadius-medium) 0 0;color:var(--fgColor-default)}:is(.tabnav-tab.selected,.tabnav-tab[aria-selected=true],.tabnav-tab[aria-current]:not([aria-current=false])) .octicon{color:inherit}.tabnav-tab:hover{color:var(--fgColor-default);-webkit-text-decoration:none;text-decoration:none;transition-duration:.1s}.tabnav-tab:focus,.tabnav-tab:focus-visible{border-radius:var(--borderRadius-medium) var(--borderRadius-medium) 0 0!important;outline-offset:-6px}.tabnav-tab .octicon,.tabnav-tab:active{color:var(--fgColor-muted)}.tabnav-tab .octicon{margin-right:var(--control-small-gap)}.tabnav-tab .Counter{color:inherit;margin-left:var(--control-small-gap)}.tabnav-extra{color:var(--fgColor-muted);display:inline-block;font-size:var(--text-body-size-small);margin-left:10px;padding-top:10px}.tabnav-extra>.octicon{margin-right:var(--base-size-2)}a.tabnav-extra:hover{color:var(--fgColor-accent);-webkit-text-decoration:none;text-decoration:none}.tabnav-btn{margin-left:var(--controlStack-medium-gap-condensed)}
@@ -4,6 +4,7 @@
4
4
  ".tabnav",
5
5
  ".tabnav-tabs",
6
6
  ".tabnav-tab",
7
+ ".tabnav-tabs>li",
7
8
  ".tabnav-tab.selected",
8
9
  ".tabnav-tab[aria-current]:not([aria-current=false])",
9
10
  ".tabnav-tab[aria-selected=true]",
@@ -1 +1 @@
1
- {"version":3,"sources":["tab_nav.pcss"],"names":[],"mappings":"AAGA,QAIE,sEAAuE,CADvE,qCAAsC,CAFtC,YAIF,CAEA,aACE,YAAa,CAEb,8CAAiD,CACjD,eACF,CAEA,YAUE,wBAA6B,CAE7B,0CAAgB,CAAhB,eAAgB,CAJhB,0BAA2B,CAP3B,oBAAqB,CACrB,aAAc,CAGd,sCAAuC,CAEvC,gBAAiB,CAHjB,uEAAwE,CAKxE,4BAAqB,CAArB,oBAAqB,CAIrB,4CA0CF,CAxCE,yGAIE,uCAAwC,CACxC,uCAAwC,CACxC,uEAAwE,CAHxE,4BAQF,CAHE,uHACE,aACF,CAGF,kBACE,4BAA6B,CAC7B,4BAAqB,CAArB,oBAAqB,CACrB,uBACF,CAEA,4CAEE,iFAAmF,CACnF,mBACF,CAMA,wCAHE,0BAOF,CAJA,qBAEE,qCAEF,CAEA,qBAGE,aAAc,CADd,oCAEF,CAQF,cAOE,0BAA2B,CAN3B,oBAAqB,CAKrB,qCAAsC,CADtC,gBAAiB,CAFjB,gBASF,CAHE,uBACE,+BACF,CAKF,qBACE,2BAA4B,CAC5B,4BAAqB,CAArB,oBACF,CAOA,YAEE,oDACF","file":"tab_nav.css","sourcesContent":["/* tabnav */\n\n/* Outer wrapper */\n.tabnav {\n margin-top: 0;\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: var(--stack-gap-normal);\n border-bottom: var(--borderWidth-thin) solid var(--borderColor-default);\n}\n\n.tabnav-tabs {\n display: flex;\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: calc(var(--borderWidth-thin) * -1);\n overflow: hidden;\n}\n\n.tabnav-tab {\n display: inline-block;\n flex-shrink: 0;\n /* stylelint-disable-next-line primer/spacing */\n padding: var(--base-size-8) var(--control-medium-paddingInline-spacious);\n font-size: var(--text-body-size-medium);\n /* stylelint-disable-next-line primer/typography */\n line-height: 23px;\n color: var(--fgColor-muted);\n text-decoration: none;\n background-color: transparent;\n border: var(--borderWidth-thin) solid transparent;\n border-bottom: 0;\n transition: color 0.2s cubic-bezier(0.3, 0, 0.5, 1);\n\n &.selected,\n &[aria-selected='true'],\n &[aria-current]:not([aria-current='false']) {\n color: var(--fgColor-default);\n background-color: var(--bgColor-default); /* cover bottom border */\n border-color: var(--borderColor-default);\n border-radius: var(--borderRadius-medium) var(--borderRadius-medium) 0 0;\n\n & .octicon {\n color: inherit;\n }\n }\n\n &:hover {\n color: var(--fgColor-default);\n text-decoration: none;\n transition-duration: 0.1s;\n }\n\n &:focus,\n &:focus-visible {\n border-radius: var(--borderRadius-medium) var(--borderRadius-medium) 0 0 !important;\n outline-offset: -6px;\n }\n\n &:active {\n color: var(--fgColor-muted);\n }\n\n & .octicon {\n /* stylelint-disable-next-line primer/spacing */\n margin-right: var(--control-small-gap);\n color: var(--fgColor-muted);\n }\n\n & .Counter {\n /* stylelint-disable-next-line primer/spacing */\n margin-left: var(--control-small-gap);\n color: inherit;\n }\n}\n\n/* Tabnav extras\n**\n** Tabnav extras are non-tab elements that sit in the tabnav. Usually they're\n** inline text or links. */\n\n.tabnav-extra {\n display: inline-block;\n /* stylelint-disable-next-line primer/spacing */\n padding-top: 10px;\n /* stylelint-disable-next-line primer/spacing */\n margin-left: 10px;\n font-size: var(--text-body-size-small);\n color: var(--fgColor-muted);\n\n & > .octicon {\n margin-right: var(--base-size-2);\n }\n}\n\n/* When tabnav-extra are anchors */\n/* stylelint-disable-next-line selector-no-qualifying-type, selector-max-type */\na.tabnav-extra:hover {\n color: var(--fgColor-accent);\n text-decoration: none;\n}\n\n/* Tabnav buttons\n**\n** For when there are multiple buttons, space them out appropriately. Requires\n** the buttons to be floated or inline-block. */\n\n.tabnav-btn {\n /* stylelint-disable-next-line primer/spacing */\n margin-left: var(--controlStack-medium-gap-condensed);\n}\n"]}
1
+ {"version":3,"sources":["tab_nav.pcss"],"names":[],"mappings":"AAGA,QAIE,sEAAuE,CADvE,qCAAsC,CAFtC,YAIF,CAEA,aACE,YAAa,CAEb,8CAAiD,CACjD,eAAgB,CAChB,iBAMF,CAEA,4BAJI,aA2DJ,CAvDA,YAUE,wBAA6B,CAE7B,0CAAgB,CAAhB,eAAgB,CAJhB,0BAA2B,CAP3B,oBAAqB,CAIrB,sCAAuC,CAEvC,gBAAiB,CAHjB,uEAAwE,CAKxE,4BAAqB,CAArB,oBAAqB,CAIrB,4CA0CF,CAxCE,yGAIE,uCAAwC,CACxC,uCAAwC,CACxC,uEAAwE,CAHxE,4BAQF,CAHE,uHACE,aACF,CAGF,kBACE,4BAA6B,CAC7B,4BAAqB,CAArB,oBAAqB,CACrB,uBACF,CAEA,4CAEE,iFAAmF,CACnF,mBACF,CAMA,wCAHE,0BAOF,CAJA,qBAEE,qCAEF,CAEA,qBAGE,aAAc,CADd,oCAEF,CAQF,cAOE,0BAA2B,CAN3B,oBAAqB,CAKrB,qCAAsC,CADtC,gBAAiB,CAFjB,gBASF,CAHE,uBACE,+BACF,CAKF,qBACE,2BAA4B,CAC5B,4BAAqB,CAArB,oBACF,CAOA,YAEE,oDACF","file":"tab_nav.css","sourcesContent":["/* tabnav */\n\n/* Outer wrapper */\n.tabnav {\n margin-top: 0;\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: var(--stack-gap-normal);\n border-bottom: var(--borderWidth-thin) solid var(--borderColor-default);\n}\n\n.tabnav-tabs {\n display: flex;\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: calc(var(--borderWidth-thin) * -1);\n overflow-x: auto;\n overflow-y: hidden;\n\n /* stylelint-disable-next-line selector-max-type */\n & > li {\n flex-shrink: 0;\n }\n}\n\n.tabnav-tab {\n display: inline-block;\n flex-shrink: 0;\n /* stylelint-disable-next-line primer/spacing */\n padding: var(--base-size-8) var(--control-medium-paddingInline-spacious);\n font-size: var(--text-body-size-medium);\n /* stylelint-disable-next-line primer/typography */\n line-height: 23px;\n color: var(--fgColor-muted);\n text-decoration: none;\n background-color: transparent;\n border: var(--borderWidth-thin) solid transparent;\n border-bottom: 0;\n transition: color 0.2s cubic-bezier(0.3, 0, 0.5, 1);\n\n &.selected,\n &[aria-selected='true'],\n &[aria-current]:not([aria-current='false']) {\n color: var(--fgColor-default);\n background-color: var(--bgColor-default); /* cover bottom border */\n border-color: var(--borderColor-default);\n border-radius: var(--borderRadius-medium) var(--borderRadius-medium) 0 0;\n\n & .octicon {\n color: inherit;\n }\n }\n\n &:hover {\n color: var(--fgColor-default);\n text-decoration: none;\n transition-duration: 0.1s;\n }\n\n &:focus,\n &:focus-visible {\n border-radius: var(--borderRadius-medium) var(--borderRadius-medium) 0 0 !important;\n outline-offset: -6px;\n }\n\n &:active {\n color: var(--fgColor-muted);\n }\n\n & .octicon {\n /* stylelint-disable-next-line primer/spacing */\n margin-right: var(--control-small-gap);\n color: var(--fgColor-muted);\n }\n\n & .Counter {\n /* stylelint-disable-next-line primer/spacing */\n margin-left: var(--control-small-gap);\n color: inherit;\n }\n}\n\n/* Tabnav extras\n**\n** Tabnav extras are non-tab elements that sit in the tabnav. Usually they're\n** inline text or links. */\n\n.tabnav-extra {\n display: inline-block;\n /* stylelint-disable-next-line primer/spacing */\n padding-top: 10px;\n /* stylelint-disable-next-line primer/spacing */\n margin-left: 10px;\n font-size: var(--text-body-size-small);\n color: var(--fgColor-muted);\n\n & > .octicon {\n margin-right: var(--base-size-2);\n }\n}\n\n/* When tabnav-extra are anchors */\n/* stylelint-disable-next-line selector-no-qualifying-type, selector-max-type */\na.tabnav-extra:hover {\n color: var(--fgColor-accent);\n text-decoration: none;\n}\n\n/* Tabnav buttons\n**\n** For when there are multiple buttons, space them out appropriately. Requires\n** the buttons to be floated or inline-block. */\n\n.tabnav-btn {\n /* stylelint-disable-next-line primer/spacing */\n margin-left: var(--controlStack-medium-gap-condensed);\n}\n"]}
@@ -12,7 +12,13 @@
12
12
  display: flex;
13
13
  /* stylelint-disable-next-line primer/spacing */
14
14
  margin-bottom: calc(var(--borderWidth-thin) * -1);
15
- overflow: hidden;
15
+ overflow-x: auto;
16
+ overflow-y: hidden;
17
+
18
+ /* stylelint-disable-next-line selector-max-type */
19
+ & > li {
20
+ flex-shrink: 0;
21
+ }
16
22
  }
17
23
 
18
24
  .tabnav-tab {
@@ -18,6 +18,7 @@ module Primer
18
18
  # @!method initialize
19
19
  #
20
20
  # @macro form_full_width_arguments
21
+ # @macro form_input_character_limit_arguments
21
22
  # @macro form_input_arguments
22
23
  # @macro form_input_width_arguments
23
24
  end
@@ -19,6 +19,7 @@ module Primer
19
19
  #
20
20
  # @macro form_size_arguments
21
21
  # @macro form_full_width_arguments
22
+ # @macro form_input_character_limit_arguments
22
23
  # @macro form_input_arguments
23
24
  # @macro form_input_width_arguments
24
25
  #
@@ -21,7 +21,7 @@
21
21
  aria: { hidden: true },
22
22
  focusable: false
23
23
  )) do %>
24
- <path fill-rule="evenodd" d="M8 2a.75.75 0 0 1 .75.75v11.5a.75.75 0 0 1-1.5 0V2.75A.75.75 0 0 1 8 2Z" />
24
+ <path fill-rule="evenodd" d="M8 2a.75.75 0 0 1 .75.75v11.5a.75.75 0 0 1-1.5 0V2.75A.75.75 0 0 1 8 2Z"></path>
25
25
  <% end %>
26
26
  <% end %>
27
27
 
@@ -36,7 +36,7 @@
36
36
  aria: { hidden: true },
37
37
  focusable: false
38
38
  )) do %>
39
- <path fill-rule="evenodd" d="M8 12.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9ZM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12Z" />
39
+ <path fill-rule="evenodd" d="M8 12.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9ZM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12Z"></path>
40
40
  <% end %>
41
41
  <% end %>
42
42
  <% end %>
@@ -9,7 +9,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
10
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
11
  };
12
- var _ToolTipElement_instances, _ToolTipElement_abortController, _ToolTipElement_align, _ToolTipElement_side, _ToolTipElement_allowUpdatePosition, _ToolTipElement_showReason, _ToolTipElement_update, _ToolTipElement_updateControl, _ToolTipElement_updateControlReference, _ToolTipElement_updateDirection, _ToolTipElement_updatePosition;
12
+ var _ToolTipElement_instances, _ToolTipElement_abortController, _ToolTipElement_align, _ToolTipElement_side, _ToolTipElement_allowUpdatePosition, _ToolTipElement_showReason, _ToolTipElement_isControlsPopoverOpen, _ToolTipElement_update, _ToolTipElement_updateControl, _ToolTipElement_updateControlReference, _ToolTipElement_updateDirection, _ToolTipElement_updatePosition;
13
13
  import '@oddbird/popover-polyfill';
14
14
  import { getAnchoredPosition } from '@primer/behaviors';
15
15
  const isPopoverOpen = (() => {
@@ -78,6 +78,7 @@ class ToolTipElement extends HTMLElement {
78
78
  _ToolTipElement_side.set(this, 'outside-bottom');
79
79
  _ToolTipElement_allowUpdatePosition.set(this, false);
80
80
  _ToolTipElement_showReason.set(this, 'mouse');
81
+ _ToolTipElement_isControlsPopoverOpen.set(this, false);
81
82
  }
82
83
  styles() {
83
84
  return `
@@ -244,18 +245,24 @@ class ToolTipElement extends HTMLElement {
244
245
  if (!this.control)
245
246
  return;
246
247
  const showing = isPopoverOpen(this);
248
+ // Track when the control's popover (e.g an ActionMenu) is opened/closed
249
+ if (event.type === 'beforetoggle' && event.currentTarget !== this) {
250
+ __classPrivateFieldSet(this, _ToolTipElement_isControlsPopoverOpen, event.newState === 'open', "f");
251
+ }
247
252
  // Ensures that tooltip stays open when hovering between tooltip and element
248
253
  // WCAG Success Criterion 1.4.13 Hoverable
249
- const shouldShow = event.type === 'mouseenter' ||
254
+ const shouldShow = (event.type === 'mouseenter' ||
250
255
  // Only show tooltip on focus if running in headless browser (for tests) or if focus ring
251
256
  // is visible (i.e. if user is using keyboard navigation)
252
- (event.type === 'focus' && (navigator.webdriver || this.control.matches(':focus-visible')));
257
+ (event.type === 'focus' && (navigator.webdriver || this.control.matches(':focus-visible')))) &&
258
+ // Don't show tooltip if the control's popover is open (e.g. an ActionMenu)
259
+ !__classPrivateFieldGet(this, _ToolTipElement_isControlsPopoverOpen, "f");
253
260
  const isMouseLeaveFromButton = event.type === 'mouseleave' &&
254
261
  event.relatedTarget !== this.control &&
255
262
  event.relatedTarget !== this;
256
263
  const isEscapeKeydown = event.type === 'keydown' && event.key === 'Escape';
257
264
  const isMouseDownOnButton = event.type === 'mousedown' && event.currentTarget === this.control;
258
- const isOpeningOtherPopover = event.type === 'beforetoggle' && event.currentTarget !== this;
265
+ const isOpeningOtherPopover = event.type === 'beforetoggle' && event.currentTarget !== this && event.newState === 'open';
259
266
  const shouldHide = isMouseLeaveFromButton || isEscapeKeydown || isMouseDownOnButton || isOpeningOtherPopover;
260
267
  if (showing && isEscapeKeydown) {
261
268
  /* eslint-disable-next-line no-restricted-syntax */
@@ -288,7 +295,7 @@ class ToolTipElement extends HTMLElement {
288
295
  }
289
296
  }
290
297
  }
291
- _ToolTipElement_abortController = new WeakMap(), _ToolTipElement_align = new WeakMap(), _ToolTipElement_side = new WeakMap(), _ToolTipElement_allowUpdatePosition = new WeakMap(), _ToolTipElement_showReason = new WeakMap(), _ToolTipElement_instances = new WeakSet(), _ToolTipElement_update = function _ToolTipElement_update(isOpen) {
298
+ _ToolTipElement_abortController = new WeakMap(), _ToolTipElement_align = new WeakMap(), _ToolTipElement_side = new WeakMap(), _ToolTipElement_allowUpdatePosition = new WeakMap(), _ToolTipElement_showReason = new WeakMap(), _ToolTipElement_isControlsPopoverOpen = new WeakMap(), _ToolTipElement_instances = new WeakSet(), _ToolTipElement_update = function _ToolTipElement_update(isOpen) {
292
299
  if (isOpen) {
293
300
  openTooltips.add(this);
294
301
  this.classList.remove(TOOLTIP_SR_ONLY_CLASS);
@@ -174,6 +174,7 @@ class ToolTipElement extends HTMLElement {
174
174
  #side: AnchorSide = 'outside-bottom'
175
175
  #allowUpdatePosition = false
176
176
  #showReason: 'focus' | 'mouse' = 'mouse'
177
+ #isControlsPopoverOpen = false
177
178
  get showReason() {
178
179
  return this.#showReason
179
180
  }
@@ -247,20 +248,28 @@ class ToolTipElement extends HTMLElement {
247
248
  if (!this.control) return
248
249
  const showing = isPopoverOpen(this)
249
250
 
251
+ // Track when the control's popover (e.g an ActionMenu) is opened/closed
252
+ if (event.type === 'beforetoggle' && event.currentTarget !== this) {
253
+ this.#isControlsPopoverOpen = (event as ToggleEvent).newState === 'open'
254
+ }
255
+
250
256
  // Ensures that tooltip stays open when hovering between tooltip and element
251
257
  // WCAG Success Criterion 1.4.13 Hoverable
252
258
  const shouldShow =
253
- event.type === 'mouseenter' ||
254
- // Only show tooltip on focus if running in headless browser (for tests) or if focus ring
255
- // is visible (i.e. if user is using keyboard navigation)
256
- (event.type === 'focus' && (navigator.webdriver || this.control.matches(':focus-visible')))
259
+ (event.type === 'mouseenter' ||
260
+ // Only show tooltip on focus if running in headless browser (for tests) or if focus ring
261
+ // is visible (i.e. if user is using keyboard navigation)
262
+ (event.type === 'focus' && (navigator.webdriver || this.control.matches(':focus-visible')))) &&
263
+ // Don't show tooltip if the control's popover is open (e.g. an ActionMenu)
264
+ !this.#isControlsPopoverOpen
257
265
  const isMouseLeaveFromButton =
258
266
  event.type === 'mouseleave' &&
259
267
  (event as MouseEvent).relatedTarget !== this.control &&
260
268
  (event as MouseEvent).relatedTarget !== this
261
269
  const isEscapeKeydown = event.type === 'keydown' && (event as KeyboardEvent).key === 'Escape'
262
270
  const isMouseDownOnButton = event.type === 'mousedown' && event.currentTarget === this.control
263
- const isOpeningOtherPopover = event.type === 'beforetoggle' && event.currentTarget !== this
271
+ const isOpeningOtherPopover =
272
+ event.type === 'beforetoggle' && event.currentTarget !== this && (event as ToggleEvent).newState === 'open'
264
273
  const shouldHide = isMouseLeaveFromButton || isEscapeKeydown || isMouseDownOnButton || isOpeningOtherPopover
265
274
 
266
275
  if (showing && isEscapeKeydown) {
@@ -1,9 +1,9 @@
1
1
  <%= render Primer::BaseComponent.new(**@system_arguments) do %>
2
2
  <span class="ActionListContent">
3
3
  <% if leading_visual %>
4
- <span class="ActionListItem-visual ActionListItem-visual--leading">
5
- <%= leading_visual %>
6
- </span>
4
+ <span class="ActionListItem-visual ActionListItem-visual--leading">
5
+ <%= leading_visual %>
6
+ </span>
7
7
  <% end %>
8
8
  <%= render(Primer::ConditionalWrapper.new(condition: description?, tag: :span, **@description_wrapper_arguments)) do %>
9
9
  <span class="ActionListItem-label"><%= content %></span>
@@ -13,9 +13,10 @@
13
13
  </span>
14
14
  <% end %>
15
15
  <% end %>
16
- <% if trailing_visual %>
16
+ <% if trailing_visual %>
17
17
  <span class="ActionListItem-visual ActionListItem-visual--trailing">
18
18
  <%= trailing_visual %>
19
19
  </span>
20
20
  <% end %>
21
+ </span>
21
22
  <% end %>
@@ -19,6 +19,10 @@ module Primer
19
19
  class Avatar < Primer::Component
20
20
  status :beta
21
21
 
22
+ def link?
23
+ @href.present?
24
+ end
25
+
22
26
  DEFAULT_SIZE = 20
23
27
  SMALL_THRESHOLD = 24
24
28
 
@@ -79,6 +79,12 @@ module Primer
79
79
  "AvatarStack--two" => avatars.size == 2,
80
80
  "AvatarStack--three-plus" => avatars.size > 2
81
81
  )
82
+
83
+ # Make non-linked stacks keyboard-focusable so :focus-within triggers expansion (avoid extra focus stop when avatars are links).
84
+ unless @tooltipped || @disable_expand
85
+ has_linked_avatars = avatars.any?(&:link?)
86
+ @body_arguments[:tabindex] ||= 0 unless has_linked_avatars
87
+ end
82
88
  end
83
89
 
84
90
  def render?
@@ -1 +1 @@
1
- .blankslate-container{container-type:inline-size;width:100%}.blankslate{--blankslate-outer-padding-block:var(--base-size-32);--blankslate-outer-padding-inline:var(--base-size-32);padding:var(--blankslate-outer-padding-block) var(--blankslate-outer-padding-inline);position:relative;text-align:center}.blankslate p{color:var(--fgColor-muted);font-size:var(--text-body-size-large)}.blankslate code{background:var(--bgColor-default);border:var(--borderWidth-thin) solid var(--borderColor-muted);border-radius:var(--borderRadius-medium);font-size:var(--text-body-size-medium);padding:var(--base-size-2) var(--base-size-4) var(--base-size-4)}.blankslate img{height:56px;width:56px}.blankslate-icon{color:var(--fgColor-muted);margin-bottom:var(--stack-gap-condensed);margin-left:var(--control-small-gap);margin-right:var(--control-small-gap)}.blankslate-image{margin-bottom:var(--stack-gap-normal)}.blankslate-heading{font-size:var(--text-title-size-medium);font-weight:var(--text-title-weight-medium);margin-bottom:var(--base-size-4)}.blankslate-action{margin-top:var(--stack-gap-normal)}.blankslate-action:first-of-type{margin-top:var(--stack-gap-spacious)}.blankslate-action:last-of-type{margin-bottom:var(--stack-gap-condensed)}.blankslate-capped{border-radius:0 0 var(--borderRadius-medium) var(--borderRadius-medium)}.blankslate-spacious{--blankslate-outer-padding-block:var(--base-size-80);--blankslate-outer-padding-inline:var(--base-size-40)}.blankslate-narrow{margin:0 auto;max-width:485px}.blankslate-large img{height:80px;width:80px}.blankslate-large h3{font-size:24px;margin:var(--stack-gap-normal) 0}.blankslate-large p{font-size:var(--text-body-size-large)}.blankslate-clean-background{border:0}@container (max-width: 34rem){.blankslate{--blankslate-outer-padding-block:var(--base-size-20);--blankslate-outer-padding-inline:var(--base-size-20)}.blankslate-spacious{--blankslate-outer-padding-block:var(--base-size-44);--blankslate-outer-padding-inline:var(--base-size-28)}.blankslate-icon{margin-bottom:var(--stack-gap-condensed)}.blankslate-heading{font-size:var(--text-title-size-small)}.blankslate p{font-size:var(--text-body-size-medium)}.blankslate-action{margin-top:var(--stack-gap-condensed)}.blankslate-action:first-of-type{margin-top:var(--stack-gap-normal)}.blankslate-action:last-of-type{margin-bottom:calc(var(--stack-gap-condensed)/2)}}
1
+ .blankslate-container{container-type:inline-size;width:100%}.blankslate{--blankslate-outer-padding-block:var(--base-size-32);--blankslate-outer-padding-inline:var(--base-size-32);padding:var(--blankslate-outer-padding-block) var(--blankslate-outer-padding-inline);position:relative;text-align:center}.blankslate p{color:var(--fgColor-muted);font-size:var(--text-body-size-large);margin:auto;max-width:56rem}.blankslate code{background:var(--bgColor-default);border:var(--borderWidth-thin) solid var(--borderColor-muted);border-radius:var(--borderRadius-medium);font-size:var(--text-body-size-medium);padding:var(--base-size-2) var(--base-size-4) var(--base-size-4)}.blankslate img{height:56px;width:56px}.blankslate-icon{color:var(--fgColor-muted);margin-bottom:var(--stack-gap-condensed);margin-left:var(--control-small-gap);margin-right:var(--control-small-gap)}.blankslate-image{margin-bottom:var(--stack-gap-normal)}.blankslate-heading{font-size:var(--text-title-size-medium);font-weight:var(--text-title-weight-medium);margin-bottom:var(--base-size-4)}.blankslate-action{margin-top:var(--stack-gap-normal)}.blankslate-action:first-of-type{margin-top:var(--stack-gap-spacious)}.blankslate-action:last-of-type{margin-bottom:var(--stack-gap-condensed)}.blankslate-capped{border-radius:0 0 var(--borderRadius-medium) var(--borderRadius-medium)}.blankslate-spacious{--blankslate-outer-padding-block:var(--base-size-80);--blankslate-outer-padding-inline:var(--base-size-40)}.blankslate-narrow{margin:0 auto;max-width:485px}.blankslate-large img{height:80px;width:80px}.blankslate-large h3{font-size:24px;margin:var(--stack-gap-normal) 0}.blankslate-large p{font-size:var(--text-body-size-large)}.blankslate-clean-background{border:0}@container (max-width: 34rem){.blankslate{--blankslate-outer-padding-block:var(--base-size-20);--blankslate-outer-padding-inline:var(--base-size-20)}.blankslate-spacious{--blankslate-outer-padding-block:var(--base-size-44);--blankslate-outer-padding-inline:var(--base-size-28)}.blankslate-icon{margin-bottom:var(--stack-gap-condensed)}.blankslate-heading{font-size:var(--text-title-size-small)}.blankslate p{font-size:var(--text-body-size-medium)}.blankslate-action{margin-top:var(--stack-gap-condensed)}.blankslate-action:first-of-type{margin-top:var(--stack-gap-normal)}.blankslate-action:last-of-type{margin-bottom:calc(var(--stack-gap-condensed)/2)}}
@@ -1 +1 @@
1
- {"version":3,"sources":["blankslate.pcss"],"names":[],"mappings":"AAEA,sBACE,0BAA2B,CAC3B,UACF,CAEA,YACE,oDAAqD,CACrD,qDAAsD,CAItD,oFAAqF,CAFrF,iBAAkB,CAGlB,iBAsBF,CAnBE,cAEE,0BAA2B,CAD3B,qCAEF,CAGA,iBAGE,iCAAkC,CAClC,6DAA8D,CAC9D,wCAAyC,CAHzC,sCAAuC,CADvC,gEAKF,CAGA,gBAEE,WAAY,CADZ,UAEF,CAGF,iBAOE,0BAA2B,CAH3B,wCAAyC,CAEzC,oCAAqC,CAJrC,qCAMF,CAEA,kBAEE,qCACF,CAEA,oBAEE,uCAAwC,CACxC,2CAA4C,CAF5C,gCAGF,CAEA,mBAEE,kCAWF,CATE,iCAEE,oCACF,CAEA,gCAEE,wCACF,CAGF,mBACE,uEACF,CAEA,qBACE,oDAAqD,CACrD,qDACF,CAEA,mBAEE,aAAc,CADd,eAEF,CAME,sBAEE,WAAY,CADZ,UAEF,CAGA,qBAME,cAAe,CAJf,gCAKF,CAGA,oBACE,qCACF,CAKF,6BACE,QACF,CAIA,8BACE,YACE,oDAAqD,CACrD,qDACF,CAEA,qBACE,oDAAqD,CACrD,qDACF,CAEA,iBAEE,wCACF,CAEA,oBACE,sCACF,CAGA,cACE,sCACF,CAEA,mBAEE,qCAWF,CATE,iCAEE,kCACF,CAEA,gCAEE,gDACF,CAEJ","file":"blankslate.css","sourcesContent":["/* blankslate */\n\n.blankslate-container {\n container-type: inline-size;\n width: 100%;\n}\n\n.blankslate {\n --blankslate-outer-padding-block: var(--base-size-32);\n --blankslate-outer-padding-inline: var(--base-size-32);\n\n position: relative;\n /* stylelint-disable-next-line primer/spacing */\n padding: var(--blankslate-outer-padding-block) var(--blankslate-outer-padding-inline);\n text-align: center;\n\n /* stylelint-disable-next-line selector-max-type */\n & p {\n font-size: var(--text-body-size-large);\n color: var(--fgColor-muted);\n }\n\n /* stylelint-disable-next-line selector-max-type */\n & code {\n padding: var(--base-size-2) var(--base-size-4) var(--base-size-4);\n font-size: var(--text-body-size-medium);\n background: var(--bgColor-default);\n border: var(--borderWidth-thin) solid var(--borderColor-muted);\n border-radius: var(--borderRadius-medium);\n }\n\n /* stylelint-disable-next-line selector-max-type */\n & img {\n width: 56px;\n height: 56px;\n }\n}\n\n.blankslate-icon {\n /* stylelint-disable-next-line primer/spacing */\n margin-right: var(--control-small-gap);\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: var(--stack-gap-condensed);\n /* stylelint-disable-next-line primer/spacing */\n margin-left: var(--control-small-gap);\n color: var(--fgColor-muted);\n}\n\n.blankslate-image {\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: var(--stack-gap-normal);\n}\n\n.blankslate-heading {\n margin-bottom: var(--base-size-4);\n font-size: var(--text-title-size-medium);\n font-weight: var(--text-title-weight-medium);\n}\n\n.blankslate-action {\n /* stylelint-disable-next-line primer/spacing */\n margin-top: var(--stack-gap-normal);\n\n &:first-of-type {\n /* stylelint-disable-next-line primer/spacing */\n margin-top: var(--stack-gap-spacious);\n }\n\n &:last-of-type {\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: var(--stack-gap-condensed);\n }\n}\n\n.blankslate-capped {\n border-radius: 0 0 var(--borderRadius-medium) var(--borderRadius-medium);\n}\n\n.blankslate-spacious {\n --blankslate-outer-padding-block: var(--base-size-80);\n --blankslate-outer-padding-inline: var(--base-size-40);\n}\n\n.blankslate-narrow {\n max-width: 485px;\n margin: 0 auto;\n}\n\n/* was .large-format\n** QUESTION: should we deprecate this? */\n.blankslate-large {\n /* stylelint-disable-next-line selector-max-type */\n & img {\n width: 80px;\n height: 80px;\n }\n\n /* stylelint-disable-next-line selector-max-type */\n & h3 {\n /* stylelint-disable-next-line primer/spacing */\n margin: var(--stack-gap-normal) 0;\n\n /* font-size: $h3-size; // This doesn't actually make the text larger. Should this be $h2-size? */\n /* stylelint-disable-next-line primer/typography */\n font-size: 24px;\n }\n\n /* stylelint-disable-next-line selector-max-type */\n & p {\n font-size: var(--text-body-size-large);\n }\n}\n\n/* was .clean-background\n** TO DO: deprecate this and use utility instead */\n.blankslate-clean-background {\n border: 0;\n}\n\n/* At the time these styles were written,\n `34rem` was our \"small\" breakpoint width */\n@container (max-width: 34rem) {\n .blankslate {\n --blankslate-outer-padding-block: var(--base-size-20);\n --blankslate-outer-padding-inline: var(--base-size-20);\n }\n\n .blankslate-spacious {\n --blankslate-outer-padding-block: var(--base-size-44);\n --blankslate-outer-padding-inline: var(--base-size-28);\n }\n\n .blankslate-icon {\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: var(--stack-gap-condensed);\n }\n\n .blankslate-heading {\n font-size: var(--text-title-size-small);\n }\n\n /* stylelint-disable-next-line selector-max-type */\n .blankslate p {\n font-size: var(--text-body-size-medium);\n }\n\n .blankslate-action {\n /* stylelint-disable-next-line primer/spacing */\n margin-top: var(--stack-gap-condensed);\n\n &:first-of-type {\n /* stylelint-disable-next-line primer/spacing */\n margin-top: var(--stack-gap-normal);\n }\n\n &:last-of-type {\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: calc(var(--stack-gap-condensed) / 2);\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["blankslate.pcss"],"names":[],"mappings":"AAEA,sBACE,0BAA2B,CAC3B,UACF,CAEA,YACE,oDAAqD,CACrD,qDAAsD,CAItD,oFAAqF,CAFrF,iBAAkB,CAGlB,iBAwBF,CArBE,cAEE,0BAA2B,CAD3B,qCAAsC,CAGtC,WAAY,CADZ,eAEF,CAGA,iBAGE,iCAAkC,CAClC,6DAA8D,CAC9D,wCAAyC,CAHzC,sCAAuC,CADvC,gEAKF,CAGA,gBAEE,WAAY,CADZ,UAEF,CAGF,iBAOE,0BAA2B,CAH3B,wCAAyC,CAEzC,oCAAqC,CAJrC,qCAMF,CAEA,kBAEE,qCACF,CAEA,oBAEE,uCAAwC,CACxC,2CAA4C,CAF5C,gCAGF,CAEA,mBAEE,kCAWF,CATE,iCAEE,oCACF,CAEA,gCAEE,wCACF,CAGF,mBACE,uEACF,CAEA,qBACE,oDAAqD,CACrD,qDACF,CAEA,mBAEE,aAAc,CADd,eAEF,CAME,sBAEE,WAAY,CADZ,UAEF,CAGA,qBAME,cAAe,CAJf,gCAKF,CAGA,oBACE,qCACF,CAKF,6BACE,QACF,CAIA,8BACE,YACE,oDAAqD,CACrD,qDACF,CAEA,qBACE,oDAAqD,CACrD,qDACF,CAEA,iBAEE,wCACF,CAEA,oBACE,sCACF,CAGA,cACE,sCACF,CAEA,mBAEE,qCAWF,CATE,iCAEE,kCACF,CAEA,gCAEE,gDACF,CAEJ","file":"blankslate.css","sourcesContent":["/* blankslate */\n\n.blankslate-container {\n container-type: inline-size;\n width: 100%;\n}\n\n.blankslate {\n --blankslate-outer-padding-block: var(--base-size-32);\n --blankslate-outer-padding-inline: var(--base-size-32);\n\n position: relative;\n /* stylelint-disable-next-line primer/spacing */\n padding: var(--blankslate-outer-padding-block) var(--blankslate-outer-padding-inline);\n text-align: center;\n\n /* stylelint-disable-next-line selector-max-type */\n & p {\n font-size: var(--text-body-size-large);\n color: var(--fgColor-muted);\n max-width: 56rem;\n margin: auto;\n }\n\n /* stylelint-disable-next-line selector-max-type */\n & code {\n padding: var(--base-size-2) var(--base-size-4) var(--base-size-4);\n font-size: var(--text-body-size-medium);\n background: var(--bgColor-default);\n border: var(--borderWidth-thin) solid var(--borderColor-muted);\n border-radius: var(--borderRadius-medium);\n }\n\n /* stylelint-disable-next-line selector-max-type */\n & img {\n width: 56px;\n height: 56px;\n }\n}\n\n.blankslate-icon {\n /* stylelint-disable-next-line primer/spacing */\n margin-right: var(--control-small-gap);\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: var(--stack-gap-condensed);\n /* stylelint-disable-next-line primer/spacing */\n margin-left: var(--control-small-gap);\n color: var(--fgColor-muted);\n}\n\n.blankslate-image {\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: var(--stack-gap-normal);\n}\n\n.blankslate-heading {\n margin-bottom: var(--base-size-4);\n font-size: var(--text-title-size-medium);\n font-weight: var(--text-title-weight-medium);\n}\n\n.blankslate-action {\n /* stylelint-disable-next-line primer/spacing */\n margin-top: var(--stack-gap-normal);\n\n &:first-of-type {\n /* stylelint-disable-next-line primer/spacing */\n margin-top: var(--stack-gap-spacious);\n }\n\n &:last-of-type {\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: var(--stack-gap-condensed);\n }\n}\n\n.blankslate-capped {\n border-radius: 0 0 var(--borderRadius-medium) var(--borderRadius-medium);\n}\n\n.blankslate-spacious {\n --blankslate-outer-padding-block: var(--base-size-80);\n --blankslate-outer-padding-inline: var(--base-size-40);\n}\n\n.blankslate-narrow {\n max-width: 485px;\n margin: 0 auto;\n}\n\n/* was .large-format\n** QUESTION: should we deprecate this? */\n.blankslate-large {\n /* stylelint-disable-next-line selector-max-type */\n & img {\n width: 80px;\n height: 80px;\n }\n\n /* stylelint-disable-next-line selector-max-type */\n & h3 {\n /* stylelint-disable-next-line primer/spacing */\n margin: var(--stack-gap-normal) 0;\n\n /* font-size: $h3-size; // This doesn't actually make the text larger. Should this be $h2-size? */\n /* stylelint-disable-next-line primer/typography */\n font-size: 24px;\n }\n\n /* stylelint-disable-next-line selector-max-type */\n & p {\n font-size: var(--text-body-size-large);\n }\n}\n\n/* was .clean-background\n** TO DO: deprecate this and use utility instead */\n.blankslate-clean-background {\n border: 0;\n}\n\n/* At the time these styles were written,\n `34rem` was our \"small\" breakpoint width */\n@container (max-width: 34rem) {\n .blankslate {\n --blankslate-outer-padding-block: var(--base-size-20);\n --blankslate-outer-padding-inline: var(--base-size-20);\n }\n\n .blankslate-spacious {\n --blankslate-outer-padding-block: var(--base-size-44);\n --blankslate-outer-padding-inline: var(--base-size-28);\n }\n\n .blankslate-icon {\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: var(--stack-gap-condensed);\n }\n\n .blankslate-heading {\n font-size: var(--text-title-size-small);\n }\n\n /* stylelint-disable-next-line selector-max-type */\n .blankslate p {\n font-size: var(--text-body-size-medium);\n }\n\n .blankslate-action {\n /* stylelint-disable-next-line primer/spacing */\n margin-top: var(--stack-gap-condensed);\n\n &:first-of-type {\n /* stylelint-disable-next-line primer/spacing */\n margin-top: var(--stack-gap-normal);\n }\n\n &:last-of-type {\n /* stylelint-disable-next-line primer/spacing */\n margin-bottom: calc(var(--stack-gap-condensed) / 2);\n }\n }\n}\n"]}
@@ -18,6 +18,8 @@
18
18
  & p {
19
19
  font-size: var(--text-body-size-large);
20
20
  color: var(--fgColor-muted);
21
+ max-width: 56rem;
22
+ margin: auto;
21
23
  }
22
24
 
23
25
  /* stylelint-disable-next-line selector-max-type */
@@ -1,7 +1,7 @@
1
1
  <%= render(Primer::BaseComponent.new(tag: :span, hidden: @hidden, data: { target: @target }, **@wrapper_arguments)) do %>
2
2
  <%= render Primer::BaseComponent.new(**@system_arguments) do %>
3
- <circle cx="8" cy="8" r="7" stroke="currentColor" stroke-opacity="0.25" stroke-width="2" vector-effect="non-scaling-stroke" fill="none" />
4
- <path d="M15 8a7.002 7.002 0 00-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" vector-effect="non-scaling-stroke" />
3
+ <circle cx="8" cy="8" r="7" stroke="currentColor" stroke-opacity="0.25" stroke-width="2" vector-effect="non-scaling-stroke" fill="none"></circle>
4
+ <path d="M15 8a7.002 7.002 0 00-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" vector-effect="non-scaling-stroke"></path>
5
5
  <% end %>
6
6
  <% if no_aria_label? %>
7
7
  <span class="sr-only"><%= @sr_text %></span>
@@ -22,6 +22,7 @@ import './beta/relative_time';
22
22
  import './alpha/tab_container';
23
23
  import '../../lib/primer/forms/primer_multi_input';
24
24
  import '../../lib/primer/forms/primer_text_field';
25
+ import '../../lib/primer/forms/primer_text_area';
25
26
  import '../../lib/primer/forms/toggle_switch_input';
26
27
  import './alpha/action_menu/action_menu_element';
27
28
  import './alpha/select_panel_element';
@@ -22,6 +22,7 @@ import './beta/relative_time';
22
22
  import './alpha/tab_container';
23
23
  import '../../lib/primer/forms/primer_multi_input';
24
24
  import '../../lib/primer/forms/primer_text_field';
25
+ import '../../lib/primer/forms/primer_text_area';
25
26
  import '../../lib/primer/forms/toggle_switch_input';
26
27
  import './alpha/action_menu/action_menu_element';
27
28
  import './alpha/select_panel_element';
@@ -22,6 +22,7 @@ import './beta/relative_time'
22
22
  import './alpha/tab_container'
23
23
  import '../../lib/primer/forms/primer_multi_input'
24
24
  import '../../lib/primer/forms/primer_text_field'
25
+ import '../../lib/primer/forms/primer_text_area'
25
26
  import '../../lib/primer/forms/toggle_switch_input'
26
27
  import './alpha/action_menu/action_menu_element'
27
28
  import './alpha/select_panel_element'
@@ -9,7 +9,7 @@ module Primer
9
9
  attr_accessor :last_request
10
10
  end
11
11
 
12
- rescue_from ActionController::InvalidAuthenticityToken, with: :handle_invalid_authenticity_token
12
+ rescue_from ActionController::InvalidCrossOriginRequest, with: :handle_invalid_cross_origin_request
13
13
 
14
14
  before_action :reject_non_ajax_request
15
15
 
@@ -29,7 +29,7 @@ module Primer
29
29
 
30
30
  private
31
31
 
32
- def handle_invalid_authenticity_token
32
+ def handle_invalid_cross_origin_request
33
33
  render status: :unauthorized, plain: "Bad CSRF token."
34
34
  end
35
35
 
@@ -8,27 +8,31 @@ class CheckBoxWithNestedForm < ApplicationForm
8
8
  custom_cities_form.text_field(
9
9
  name: :custom_cities,
10
10
  label: "Custom cities",
11
- description: "A space-separated list of cities"
11
+ caption: "A space-separated list of cities"
12
12
  )
13
13
  end
14
14
  end
15
15
 
16
16
  form do |check_form|
17
- check_form.check_box_group(name: :city_categories) do |check_group|
17
+ check_form.check_box_group(
18
+ name: :city_categories,
19
+ label: "City categories",
20
+ caption: "Select all that apply."
21
+ ) do |check_group|
18
22
  check_group.check_box(
19
23
  value: "capital",
20
24
  label: "Capital",
21
- description: "The capital city"
25
+ caption: "The capital city"
22
26
  )
23
27
  check_group.check_box(
24
28
  value: "populous",
25
29
  label: "Most-populous",
26
- description: "The five most-populous cities"
30
+ caption: "The five most-populous cities"
27
31
  )
28
32
  check_group.check_box(
29
33
  value: "custom",
30
34
  label: "Custom",
31
- description: "A custom list of cities"
35
+ caption: "A custom list of cities"
32
36
  ) do |custom_check_box|
33
37
  custom_check_box.nested_form do |builder|
34
38
  CustomCitiesForm.new(builder)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ class TextAreaWithCharacterLimitForm < ApplicationForm
5
+ form do |my_form|
6
+ my_form.text_area(
7
+ name: :bio,
8
+ label: "Bio",
9
+ caption: "Tell us about yourself",
10
+ character_limit: 100
11
+ )
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ class TextFieldWithCharacterLimitForm < ApplicationForm
5
+ form do |my_form|
6
+ my_form.text_field(
7
+ name: :username,
8
+ label: "Username",
9
+ caption: "Choose a unique username",
10
+ character_limit: 20
11
+ )
12
+ end
13
+ end
@@ -1,10 +1,17 @@
1
- <% if @input.caption? && !@input.caption.blank? %>
2
- <span class="FormControl-caption" id="<%= @input.caption_id %>"><%= @input.caption %></span>
3
- <% elsif caption_template? %>
4
- <% caption_template = render_caption_template %>
5
- <% unless caption_template.blank? %>
6
- <span class="FormControl-caption" id="<%= @input.caption_id %>">
7
- <%= caption_template %>
8
- </span>
9
- <% end %>
1
+ <% if @input.character_limit? %>
2
+ <span class="sr-only" data-target="<%= @input.character_limit_target_prefix %>.characterLimitSrElement" aria-live="polite" role="status"></span>
3
+ <span class="sr-only" id="<%= @input.character_limit_id %>">You can enter up to <%= @input.character_limit %> <%= @input.character_limit == 1 ? 'character' : 'characters' %></span>
4
+ <span class="FormControl-caption" data-target="<%= @input.character_limit_target_prefix %>.characterLimitElement" data-max-length="<%= @input.character_limit %>" aria-hidden="true">
5
+ <span class="FormControl-caption-icon" hidden><%= render(Primer::Beta::Octicon.new(icon: :"alert-fill", size: :xsmall, aria: { hidden: true })) %></span>
6
+ <span class="FormControl-caption-text"><%= @input.character_limit %> <%= @input.character_limit == 1 ? 'character' : 'characters' %> remaining</span>
7
+ </span>
8
+ <% end %>
9
+ <% if @input.caption? || caption_template? %>
10
+ <span class="FormControl-caption" id="<%= @input.caption_id %>">
11
+ <% if @input.caption? && !@input.caption.blank? %>
12
+ <%= @input.caption %>
13
+ <% elsif caption_template? %>
14
+ <%= render_caption_template %>
15
+ <% end %>
16
+ </span>
10
17
  <% end %>
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared character counting functionality for text inputs with character limits.
3
+ * Handles real-time character count updates, validation, and aria-live announcements.
4
+ */
5
+ export declare class CharacterCounter {
6
+ private inputElement;
7
+ private characterLimitElement;
8
+ private characterLimitSrElement;
9
+ private SCREEN_READER_DELAY;
10
+ private announceTimeout;
11
+ private isInitialLoad;
12
+ constructor(inputElement: HTMLInputElement | HTMLTextAreaElement, characterLimitElement: HTMLElement, characterLimitSrElement: HTMLElement);
13
+ /**
14
+ * Initialize character counting by setting up event listener and initial count
15
+ */
16
+ initialize(signal?: AbortSignal): void;
17
+ /**
18
+ * Clean up any pending timeouts
19
+ */
20
+ cleanup(): void;
21
+ /**
22
+ * Pluralizes a word based on the count
23
+ */
24
+ private pluralize;
25
+ /**
26
+ * Update the character count display and validation state
27
+ */
28
+ private updateCharacterCount;
29
+ /**
30
+ * Announce character count to screen readers with debouncing
31
+ */
32
+ private announceToScreenReader;
33
+ /**
34
+ * Set error when character limit is exceeded
35
+ */
36
+ private setError;
37
+ /**
38
+ * Clear error when back under character limit
39
+ */
40
+ private clearError;
41
+ }