diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 3bfff1e5e0..b984203ef6 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,51 @@
+* Add support for the HTML picture tag. It supports passing a String, an Array or a Block.
+ Supports passing properties directly to the img tag via the `:image` key.
+ Since the picture tag requires an img tag, the last element you provide will be used for the img tag.
+ For complete control over the picture tag, a block can be passed, which will populate the contents of the tag accordingly.
+
+ Can be used like this for a single source:
+ ```erb
+ <%= picture_tag("picture.webp") %>
+ ```
+ which will generate the following:
+ ```html
+
+ ```
+
+ For multiple sources:
+ ```erb
+ <%= picture_tag("picture.webp", "picture.png", :class => "mt-2", :image => { alt: "Image", class: "responsive-img" }) %>
+ ```
+ will generate:
+ ```html
+
+ ```
+
+ Full control via a block:
+ ```erb
+ <%= picture_tag(:class => "my-class") do %>
+ <%= tag(:source, :srcset => image_path("picture.webp")) %>
+ <%= tag(:source, :srcset => image_path("picture.png")) %>
+ <%= image_tag("picture.png", :alt => "Image") %>
+ <% end %>
+ ```
+ will generate:
+ ```html
+
+ ```
+
+ *Juan Pablo Balarini*
+
* Remove deprecated support to passing instance variables as locals to partials.
*Rafael Mendonça França*
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index e42177386d..1acf3c0a7f 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -435,6 +435,63 @@ def image_tag(source, options = {})
tag("img", options)
end
+ # Returns an HTML picture tag for the +sources+. If +sources+ is a string,
+ # a single picture tag will be returned. If +sources+ is an array, a picture
+ # tag with nested source tags for each source will be returned. The
+ # +sources+ can be full paths, files that exist in your public images
+ # directory, or Active Storage attachments. Since the picture tag requires
+ # an img tag, the last element you provide will be used for the img tag.
+ # For complete control over the picture tag, a block can be passed, which
+ # will populate the contents of the tag accordingly.
+ #
+ # ==== Options
+ #
+ # When the last parameter is a hash you can add HTML attributes using that
+ # parameter. Apart from all the HTML supported options, the following are supported:
+ #
+ # * :image - Hash of options that are passed directly to the +image_tag+ helper.
+ #
+ # ==== Examples
+ #
+ # picture_tag("picture.webp")
+ # # =>
+ # picture_tag("gold.png", :image => { :size => "20" }
+ # # =>
+ # picture_tag("gold.png", :image => { :size => "45x70" })
+ # # =>
+ # picture_tag("picture.webp", "picture.png")
+ # # =>
+ # picture_tag("picture.webp", "picture.png", :image => { alt: "Image" })
+ # # =>
+ # picture_tag(["picture.webp", "picture.png"], :image => { alt: "Image" })
+ # # =>
+ # picture_tag(:class => "my-class") { tag(:source, :srcset => image_path("picture.webp")) + image_tag("picture.png", :alt => "Image") }
+ # # =>
+ # picture_tag { tag(:source, :srcset => image_path("picture-small.webp"), :media => "(min-width: 600px)") + tag(:source, :srcset => image_path("picture-big.webp")) + image_tag("picture.png", :alt => "Image") }
+ # # =>
+ #
+ # Active Storage blobs (images that are uploaded by the users of your app):
+ #
+ # picture_tag(user.profile_picture)
+ # # =>
+ def picture_tag(*sources, &block)
+ sources.flatten!
+ options = sources.extract_options!.symbolize_keys
+ picture_options = options.except(:image)
+ image_options = options.fetch(:image, {})
+ skip_pipeline = options.delete(:skip_pipeline)
+ source_tags = []
+
+ content_tag(:picture, picture_options) do
+ if block.present?
+ capture(&block).html_safe
+ else
+ source_tags = sources.map { |source| tag("source", srcset: resolve_asset_source("image", source, skip_pipeline)) } if sources.size > 1
+ safe_join(source_tags << image_tag(sources.last, image_options))
+ end
+ end
+ end
+
# Returns an HTML video tag for the +sources+. If +sources+ is a string,
# a single video tag will be returned. If +sources+ is an array, a video
# tag with nested source tags for each source will be returned. The
diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb
index ae61e42fec..d5bdadb3ef 100644
--- a/actionview/test/template/asset_tag_helper_test.rb
+++ b/actionview/test/template/asset_tag_helper_test.rb
@@ -247,6 +247,68 @@ def content_security_policy_nonce
%(image_tag("rss.gif", srcset: [["pic_640.jpg", "640w"], ["pic_1024.jpg", "1024w"]])) => %()
}
+ PicturePathToTag = {
+ %(image_path("xml")) => %(/images/xml),
+ %(image_path("xml.webp")) => %(/images/xml.webp),
+ %(image_path("dir/xml.webp")) => %(/images/dir/xml.webp),
+ %(image_path("/dir/xml.webp")) => %(/dir/xml.webp)
+ }
+
+ PathToPictureToTag = {
+ %(path_to_image("xml")) => %(/images/xml),
+ %(path_to_image("xml.webp")) => %(/images/xml.webp),
+ %(path_to_image("dir/xml.webp")) => %(/images/dir/xml.webp),
+ %(path_to_image("/dir/xml.webp")) => %(/dir/xml.webp)
+ }
+
+ PictureUrlToTag = {
+ %(image_url("xml")) => %(http://www.example.com/images/xml),
+ %(image_url("xml.webp")) => %(http://www.example.com/images/xml.webp),
+ %(image_url("dir/xml.webp")) => %(http://www.example.com/images/dir/xml.webp),
+ %(image_url("/dir/xml.webp")) => %(http://www.example.com/dir/xml.webp)
+ }
+
+ UrlToPictureToTag = {
+ %(url_to_image("xml")) => %(http://www.example.com/images/xml),
+ %(url_to_image("xml.webp")) => %(http://www.example.com/images/xml.webp),
+ %(url_to_image("dir/xml.webp")) => %(http://www.example.com/images/dir/xml.webp),
+ %(url_to_image("/dir/xml.webp")) => %(http://www.example.com/dir/xml.webp)
+ }
+
+ PictureLinkToTag = {
+ %(picture_tag("picture.webp")) => %(),
+ %(picture_tag("gold.png", :image => { :size => "20" })) => %(),
+ %(picture_tag("gold.png", :image => { :size => 20 })) => %(),
+ %(picture_tag("silver.png", :image => { :size => "90.9" })) => %(),
+ %(picture_tag("silver.png", :image => { :size => 90.9 })) => %(),
+ %(picture_tag("gold.png", :image => { :size => "45x70" })) => %(),
+ %(picture_tag("gold.png", :image => { "size" => "45x70" })) => %(),
+ %(picture_tag("silver.png", :image => { :size => "67.12x74.09" })) => %(),
+ %(picture_tag("silver.png", :image => { "size" => "67.12x74.09" })) => %(),
+ %(picture_tag("bronze.png", :image => { :size => "10x15.7" })) => %(),
+ %(picture_tag("bronze.png", :image => { "size" => "10x15.7" })) => %(),
+ %(picture_tag("platinum.png", :image => { :size => "4.9x20" })) => %(),
+ %(picture_tag("platinum.png", :image => { "size" => "4.9x20" })) => %(),
+ %(picture_tag("error.png", :image => { "size" => "45 x 70" })) => %(),
+ %(picture_tag("error.png", :image => { "size" => "1,024x768" })) => %(),
+ %(picture_tag("error.png", :image => { "size" => "768x1,024" })) => %(),
+ %(picture_tag("error.png", :image => { "size" => "x" })) => %(),
+ %(picture_tag("google.com.png")) => %(),
+ %(picture_tag("slash..png")) => %(),
+ %(picture_tag(".pdf.png")) => %(),
+ %(picture_tag("http://www.rubyonrails.com/images/rails.png")) => %(),
+ %(picture_tag("//www.rubyonrails.com/images/rails.png")) => %(),
+ %(picture_tag("mouse.png", :image => { :alt => nil })) => %(),
+ %(picture_tag("data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", :image => { :alt => nil })) => %(),
+ %(picture_tag("")) => %(),
+ %(picture_tag("picture.webp", "picture.png")) => %(),
+ %(picture_tag("picture.webp", "picture.png", :class => "my-class")) => %(),
+ %(picture_tag("picture.webp", "picture.png", :image => { alt: "Image" })) => %(),
+ %(picture_tag(["picture.webp", "picture.png"], :image => { alt: "Image" })) => %(),
+ %(picture_tag(:class => "my-class") { tag(:source, :srcset => image_path("picture.webp")) + image_tag("picture.png", :alt => "Image") }) => %(),
+ %(picture_tag { tag(:source, :srcset => image_path("picture-small.webp"), :media => "(min-width: 600px)") + tag(:source, :srcset => image_path("picture-big.webp")) + image_tag("picture.png", :alt => "Image") }) => %(),
+ }
+
FaviconLinkToTag = {
%(favicon_link_tag) => %(),
%(favicon_link_tag 'favicon.ico') => %(),
@@ -711,6 +773,26 @@ def test_preload_link_tag
PreloadLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end
+ def test_picture_path
+ PicturePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_path_to_picture_alias_for_picture_path
+ PathToPictureToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_picture_url
+ PictureUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_url_to_picture_alias_for_picture_url
+ UrlToPictureToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_picture_tag
+ PictureLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
def test_video_path
VideoPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end