Repository: Liquipedia/Lua-Modules
Pull request #5539: refactor(mainpage): filter buttons into widgets
Author: login: ElectricalBoy
Labels: name: c: filter_buttons
name: c: main_page
Created: 2025-03-03T02:29:10Z | Merged: 2025-03-05T13:39:38Z

## Summary

This PR converts FilterButtons to return widgets instead of html.

## How did you test this change?

dev in LoL / Overwatch


Diff:
diff --git a/components/filter_buttons/commons/filter_buttons.lua b/components/filter_buttons/commons/filter_buttons.lua
index 7faf130c116..489d7be957b 100644
--- a/components/filter_buttons/commons/filter_buttons.lua
+++ b/components/filter_buttons/commons/filter_buttons.lua
@@ -6,146 +6,16 @@
 -- Please see https://github.com/Liquipedia/Lua-Modules to contribute
 --
 
-local Array = require('Module:Array')
-local Class = require('Module:Class')
-local FnUtil = require('Module:FnUtil')
-local I18n = require('Module:I18n')
-local Logic = require('Module:Logic')
-local Lua = require('Module:Lua')
-local String = require('Module:StringUtils')
-local Variables = require('Module:Variables')
-local Table = require('Module:Table')
-
-local FilterButtons = {}
-
-local DROPDOWN_ARROW = '&#8203;▼&#8203;'
-
----@class FilterButtonCategory
----@field name string
----@field property string
----@field items string[]?
----@field defaultItems string[]?
----@field defaultItem string?
----@field itemToPropertyValues? fun(item: string): string?
----@field itemIsValid? fun(item: string): boolean
----@field transform? fun(item: string): string?
----@field expandKey string?
----@field expandable boolean?
----@field order? fun(a: string, b: string): boolean
----@field load? fun(cat: FilterButtonCategory)
----@field hasFeatured boolean?
----@field featuredByDefault boolean?
-
----Builds filterbuttons based on config stored in Module:FilterButtons/Config
----Can be used from wikicode
----@return Html
-function FilterButtons.getFromConfig()
-	return FilterButtons.get(Lua.import('Module:FilterButtons/Config').categories)
-end
-
----Entrypoint building a set of FilterButtons
----@param categories FilterButtonCategory[]
----@return Html
-function FilterButtons.get(categories)
-	Array.forEach(categories, FilterButtons._loadCategories)
-
-	local div = mw.html.create('div')
-
-	for _, category in ipairs(categories) do
-		-- Variable used to pass default values on to other modules using these.
-		assert(
-			Table.isNotEmpty(category.items), category.name .. ': List of items is required, either input or filled during load'
-		)
-		Variables.varDefine('filterbuttons_defaults_' .. category.name, table.concat(category.defaultItems, ','))
-		local buttons = FilterButtons.getButtonRow(category)
-		div:node(buttons)
-	end
-
-	return div
-end
+-- only needed for not breaking usage from wiki code.
+-- will be kicked after merge + conversion runs
 
----@param category FilterButtonCategory
-function FilterButtons._loadCategories(category)
-	if category.load then
-		category.load(category)
-	end
-
-	if category.order then
-		Array.orderInPlaceBy(category.items, category.order)
-	end
-
-	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
-	category.defaultItems = Array.map(Logic.emptyOr(category.defaultItems, category.items), itemToPropertyValues)
-end
-
----@param category FilterButtonCategory
----@return Html
-function FilterButtons.getButtonRow(category)
-	local buttons = mw.html.create('div')
-		:addClass('filter-buttons')
-		:attr('data-filter', 'data-filter')
-		:attr('data-filter-effect','fade')
-		:attr('data-filter-group', 'filterbuttons-' .. category.name)
-		:attr('data-filter-default-curated', category.featuredByDefault and 'true' or nil)
-		:tag('span')
-			:addClass('filter-button')
-			:addClass('filter-button-all')
-			:attr('data-filter-on', 'all')
-			:wikitext(I18n.translate('filterbuttons-all'))
-			:done()
-
-	local makeButton = function(value, text)
-		local button = mw.html.create('span')
-			:addClass('filter-button')
-			:attr('data-filter-on', value)
-			:wikitext(text)
-		if Table.includes(category.defaultItems, value) then
-			button:addClass('filter-button--active')
-		end
-		buttons:node(button)
-	end
-
-	if category.hasFeatured then
-		makeButton('curated', I18n.translate('filterbuttons-featured'))
-	end
-
-	local transformValueToText = category.transform or FnUtil.identity
-	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
-	for _, value in ipairs(category.items or {}) do
-		local text = transformValueToText(value)
-		local filterValue = itemToPropertyValues(value) or value
-		makeButton(filterValue, text)
-	end
-
-	if String.isNotEmpty(category.expandKey) then
-		local dropdownButton = mw.html.create('div')
-			:addClass('filter-buttons')
-			:attr('data-filter', 'data-filter')
-			:attr('data-filter-effect','fade')
-			:attr('data-filter-group', 'tournaments-list-dropdown-' .. category.expandKey)
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:addClass('filter-button-dropdown')
-				:attr('data-filter-on', 'dropdown-' .. category.expandKey)
-				:wikitext(DROPDOWN_ARROW))
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:css('display','none')
-				:attr('data-filter-on', 'all'))
-
-		buttons:node(dropdownButton)
-	end
+local Lua = require('Module:Lua')
+local FilterButtonsWidget = Lua.import('Module:Widget/FilterButtons')
 
-	if category.expandable then
-		local section = mw.html.create('div')
-			:addClass('filter-category--hidden')
-			:attr('data-filter-group', 'tournaments-list-dropdown-' .. category.name)
-			:attr('data-filter-category', 'dropdown-' .. category.name)
-			:node(buttons)
-		return section
-	end
+local LegacyFilterButtonWrapper = {}
 
-	return buttons
+function LegacyFilterButtonWrapper.getFromConfig()
+	return FilterButtonsWidget()
 end
 
-return Class.export(FilterButtons)
+return LegacyFilterButtonWrapper
diff --git a/components/main_page/wikis/apexlegends/main_page_layout_data.lua b/components/main_page/wikis/apexlegends/main_page_layout_data.lua
index b662f52144b..af5787e8f08 100644
--- a/components/main_page/wikis/apexlegends/main_page_layout_data.lua
+++ b/components/main_page/wikis/apexlegends/main_page_layout_data.lua
@@ -47,7 +47,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/arenafps/main_page_layout_data.lua b/components/main_page/wikis/arenafps/main_page_layout_data.lua
index ed6c2c4771c..2466fc84237 100644
--- a/components/main_page/wikis/arenafps/main_page_layout_data.lua
+++ b/components/main_page/wikis/arenafps/main_page_layout_data.lua
@@ -47,7 +47,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/brawlstars/main_page_layout_data.lua b/components/main_page/wikis/brawlstars/main_page_layout_data.lua
index 7e592aaf77b..4d85a023e9f 100644
--- a/components/main_page/wikis/brawlstars/main_page_layout_data.lua
+++ b/components/main_page/wikis/brawlstars/main_page_layout_data.lua
@@ -47,7 +47,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/deadlock/main_page_layout_data.lua b/components/main_page/wikis/deadlock/main_page_layout_data.lua
index 7d2c704a633..1375e63e096 100644
--- a/components/main_page/wikis/deadlock/main_page_layout_data.lua
+++ b/components/main_page/wikis/deadlock/main_page_layout_data.lua
@@ -57,7 +57,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/dota2/main_page_layout_data.lua b/components/main_page/wikis/dota2/main_page_layout_data.lua
index e2597add4bf..dcc5fe70472 100644
--- a/components/main_page/wikis/dota2/main_page_layout_data.lua
+++ b/components/main_page/wikis/dota2/main_page_layout_data.lua
@@ -102,7 +102,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-		'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+		'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/fortnite/main_page_layout_data.lua b/components/main_page/wikis/fortnite/main_page_layout_data.lua
index 66f6f9b2262..cb9387157a2 100644
--- a/components/main_page/wikis/fortnite/main_page_layout_data.lua
+++ b/components/main_page/wikis/fortnite/main_page_layout_data.lua
@@ -47,7 +47,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	tournaments = {
 		heading = 'Tournaments',
diff --git a/components/main_page/wikis/halo/main_page_layout_data.lua b/components/main_page/wikis/halo/main_page_layout_data.lua
index 1b8f55c419b..e02c136e7d5 100644
--- a/components/main_page/wikis/halo/main_page_layout_data.lua
+++ b/components/main_page/wikis/halo/main_page_layout_data.lua
@@ -47,7 +47,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/hearthstone/main_page_layout_data.lua b/components/main_page/wikis/hearthstone/main_page_layout_data.lua
index 89c88765d06..ccb023e6929 100644
--- a/components/main_page/wikis/hearthstone/main_page_layout_data.lua
+++ b/components/main_page/wikis/hearthstone/main_page_layout_data.lua
@@ -47,7 +47,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/leagueoflegends/main_page_layout_data.lua b/components/main_page/wikis/leagueoflegends/main_page_layout_data.lua
index 0485e8f5205..f150dcc5371 100644
--- a/components/main_page/wikis/leagueoflegends/main_page_layout_data.lua
+++ b/components/main_page/wikis/leagueoflegends/main_page_layout_data.lua
@@ -47,7 +47,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/marvelrivals/main_page_layout_data.lua b/components/main_page/wikis/marvelrivals/main_page_layout_data.lua
index a28d1f88c00..a11643a3717 100644
--- a/components/main_page/wikis/marvelrivals/main_page_layout_data.lua
+++ b/components/main_page/wikis/marvelrivals/main_page_layout_data.lua
@@ -55,7 +55,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/overwatch/main_page_layout_data.lua b/components/main_page/wikis/overwatch/main_page_layout_data.lua
index 6e9048b5ff2..5b1d6d93305 100644
--- a/components/main_page/wikis/overwatch/main_page_layout_data.lua
+++ b/components/main_page/wikis/overwatch/main_page_layout_data.lua
@@ -47,7 +47,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/pubg/main_page_layout_data.lua b/components/main_page/wikis/pubg/main_page_layout_data.lua
index da86073114d..fe1a5879374 100644
--- a/components/main_page/wikis/pubg/main_page_layout_data.lua
+++ b/components/main_page/wikis/pubg/main_page_layout_data.lua
@@ -47,7 +47,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/pubgmobile/main_page_layout_data.lua b/components/main_page/wikis/pubgmobile/main_page_layout_data.lua
index 9cfef6db8be..a48feb0008d 100644
--- a/components/main_page/wikis/pubgmobile/main_page_layout_data.lua
+++ b/components/main_page/wikis/pubgmobile/main_page_layout_data.lua
@@ -48,7 +48,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/rainbowsix/main_page_layout_data.lua b/components/main_page/wikis/rainbowsix/main_page_layout_data.lua
index 0daa39cbb88..870657a60d4 100644
--- a/components/main_page/wikis/rainbowsix/main_page_layout_data.lua
+++ b/components/main_page/wikis/rainbowsix/main_page_layout_data.lua
@@ -47,7 +47,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/main_page/wikis/valorant/main_page_layout_data.lua b/components/main_page/wikis/valorant/main_page_layout_data.lua
index 9dd293a1323..84ab4f2ed0a 100644
--- a/components/main_page/wikis/valorant/main_page_layout_data.lua
+++ b/components/main_page/wikis/valorant/main_page_layout_data.lua
@@ -33,7 +33,7 @@ local CONTENT = {
 	filterButtons = {
 		noPanel = true,
 		body = '<div style{{=}}"width:100%;margin-bottom:8px;">' ..
-			'{{#invoke:Lua|invoke|module=FilterButtons|fn=getFromConfig}}</div>',
+			'{{#invoke:Lua|invoke|module=Widget/Factory|fn=fromTemplate|widget=FilterButtons}}</div>',
 	},
 	matches = {
 		heading = 'Matches',
diff --git a/components/widget/filter_buttons/widget_filter_buttons.lua b/components/widget/filter_buttons/widget_filter_buttons.lua
new file mode 100644
index 00000000000..d68ccd75f6a
--- /dev/null
+++ b/components/widget/filter_buttons/widget_filter_buttons.lua
@@ -0,0 +1,121 @@
+---
+-- @Liquipedia
+-- wiki=commons
+-- page=Module:Widget/FilterButtons
+--
+-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
+--
+
+local Array = require('Module:Array')
+local Class = require('Module:Class')
+local FnUtil = require('Module:FnUtil')
+local Logic = require('Module:Logic')
+local Lua = require('Module:Lua')
+local Variables = require('Module:Variables')
+local Table = require('Module:Table')
+
+local HtmlWidgets = Lua.import('Module:Widget/Html/All')
+local Div = HtmlWidgets.Div
+local FilterButton = Lua.import('Module:Widget/FilterButtons/Button')
+local FilterButtonRow = Lua.import('Module:Widget/FilterButtons/ButtonRow')
+local Widget = Lua.import('Module:Widget')
+
+---@class FilterButtons: Widget
+---@operator call(table): FilterButtons
+local FilterButtons = Class.new(Widget)
+
+---@class FilterButtonCategory
+---@field name string
+---@field property string
+---@field items string[]?
+---@field defaultItems string[]?
+---@field defaultItem string?
+---@field itemToPropertyValues? fun(item: string): string?
+---@field itemIsValid? fun(item: string): boolean
+---@field transform? fun(item: string): string?
+---@field expandKey string?
+---@field expandable boolean?
+---@field order? fun(a: string, b: string): boolean
+---@field load? fun(cat: FilterButtonCategory)
+---@field hasFeatured boolean?
+---@field featuredByDefault boolean?
+
+---@return Widget
+function FilterButtons:render()
+	---@type FilterButtonCategory[]
+	local categories = self.props.categories or Lua.import('Module:FilterButtons/Config').categories
+
+	Array.forEach(categories, FilterButtons._loadCategories)
+
+	return Div{
+		children = Array.map(categories, function (category)
+			-- Variable used to pass default values on to other modules using these.
+			assert(
+				Table.isNotEmpty(category.items), category.name .. ': List of items is required, either input or filled during load'
+			)
+			Variables.varDefine('filterbuttons_defaults_' .. category.name, table.concat(category.defaultItems, ','))
+			return FilterButtons.getButtonRow(category)
+		end)
+	}
+end
+
+---@param category FilterButtonCategory
+function FilterButtons._loadCategories(category)
+	if category.load then
+		category.load(category)
+	end
+
+	if category.order then
+		Array.orderInPlaceBy(category.items, category.order)
+	end
+
+	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
+	category.defaultItems = Array.map(Logic.emptyOr(category.defaultItems, category.items), itemToPropertyValues)
+end
+
+---@param category FilterButtonCategory
+---@param value string?
+---@param text string?
+---@return FilterButton
+function FilterButtons._makeButton(category, value, text)
+	return FilterButton{
+		active = Table.includes(category.defaultItems, value),
+		value = value,
+		display = text
+	}
+end
+
+---@param category FilterButtonCategory
+---@return Widget
+function FilterButtons.getButtonRow(category)
+	local transformValueToText = category.transform or FnUtil.identity
+	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
+	local makeButton = FnUtil.curry(FilterButtons._makeButton, category)
+
+	local buttons = FilterButtonRow{
+		categoryName = category.name,
+		featuredByDefault = category.featuredByDefault,
+		hasFeatured = category.hasFeatured,
+		buttons = Array.map(category.items or {}, function (value)
+			local text = transformValueToText(value)
+			local filterValue = itemToPropertyValues(value) or value
+			return makeButton(filterValue, text)
+		end),
+		expandKey = category.expandKey
+	}
+
+	if category.expandable then
+		return Div{
+			classes = { 'filter-category--hidden' },
+			attributes = {
+				['data-filter-group'] = 'tournaments-list-dropdown-' .. category.name,
+				['data-filter-category'] = 'dropdown-' .. category.name
+			},
+			children = { buttons }
+		}
+	end
+
+	return buttons
+end
+
+return FilterButtons
diff --git a/components/widget/filter_buttons/widget_filter_buttons_button.lua b/components/widget/filter_buttons/widget_filter_buttons_button.lua
new file mode 100644
index 00000000000..c13aef3376e
--- /dev/null
+++ b/components/widget/filter_buttons/widget_filter_buttons_button.lua
@@ -0,0 +1,42 @@
+---
+-- @Liquipedia
+-- wiki=commons
+-- page=Module:Widget/FilterButtons/Button
+--
+-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
+--
+
+local Array = require('Module:Array')
+local Class = require('Module:Class')
+local Logic = require('Module:Logic')
+local Lua = require('Module:Lua')
+
+local Widget = Lua.import('Module:Widget')
+local HtmlWidgets = Lua.import('Module:Widget/Html/All')
+
+---@class FilterButtonParameters
+---@field buttonClasses string[]?
+---@field css table<string,string>?
+---@field active boolean?
+---@field value string?
+---@field display string|Widget|Html|nil
+
+---@class FilterButton: Widget
+---@operator call(table): FilterButton
+---@field props FilterButtonParameters
+local FilterButton = Class.new(Widget)
+
+---@return Widget
+function FilterButton:render()
+	return HtmlWidgets.Span{
+		classes = Array.extend({
+			'filter-button',
+			Logic.readBool(self.props.active) and 'filter-button--active' or nil
+		}, self.props.buttonClasses),
+		attributes = { ['data-filter-on'] = self.props.value },
+		css = self.props.css,
+		children = { self.props.display }
+	}
+end
+
+return FilterButton
diff --git a/components/widget/filter_buttons/widget_filter_buttons_row.lua b/components/widget/filter_buttons/widget_filter_buttons_row.lua
new file mode 100644
index 00000000000..035cda5b187
--- /dev/null
+++ b/components/widget/filter_buttons/widget_filter_buttons_row.lua
@@ -0,0 +1,78 @@
+---
+-- @Liquipedia
+-- wiki=commons
+-- page=Module:Widget/FilterButtons/ButtonRow
+--
+-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
+--
+
+local Class = require('Module:Class')
+local I18n = require('Module:I18n')
+local Lua = require('Module:Lua')
+local String = require('Module:StringUtils')
+
+local Widget = Lua.import('Module:Widget')
+local FilterButton = Lua.import('Module:Widget/FilterButtons/Button')
+local HtmlWidgets = Lua.import('Module:Widget/Html/All')
+local IconFa = Lua.import('Module:Widget/Image/Icon/Fontawesome')
+local WidgetUtil = Lua.import('Module:Widget/Util')
+
+---@class FilterButtonRowParameters
+---@field categoryName string
+---@field buttons (FilterButton?)[]
+---@field expandKey string?
+---@field featuredByDefault boolean?
+---@field hasFeatured boolean?
+
+local CLASS_NAME = 'filter-buttons'
+
+---@class FilterButtonRow: Widget
+---@operator call(table): FilterButtonRow
+---@field props FilterButtonRowParameters
+local FilterButtonRow = Class.new(Widget)
+
+---@return Widget
+function FilterButtonRow:render()
+	return HtmlWidgets.Div{
+		classes = {CLASS_NAME},
+		attributes = {
+			['data-filter'] = 'data-filter',
+			['data-filter-effect'] = 'fade',
+			['data-filter-group'] = 'filterbuttons-' .. self.props.categoryName,
+			['data-filter-default-curated'] = self.props.featuredByDefault and 'true' or nil,
+		},
+		children = WidgetUtil.collect(
+			FilterButton{
+				buttonClasses = { 'filter-button-all' },
+				value = 'all',
+				display = I18n.translate('filterbuttons-all')
+			},
+			self.props.hasFeatured and FilterButton{
+				value = 'curated',
+				display = I18n.translate('filterbuttons-featured')
+			},
+			self.props.buttons,
+			String.isNotEmpty(self.props.expandKey) and HtmlWidgets.Div{
+				classes = {CLASS_NAME},
+				attributes = {
+					['data-filter'] = 'data-filter',
+					['data-filter-effect'] ='fade',
+					['data-filter-group'] = 'tournaments-list-dropdown-' .. self.props.expandKey
+				},
+				children = {
+					FilterButton{
+						buttonClasses = { 'filter-button-dropdown' },
+						value = 'dropdown-' .. self.props.expandKey,
+						display = IconFa{iconName = 'expand'}
+					},
+					FilterButton{
+						css = { display = 'none' },
+						value = 'all'
+					}
+				}
+			} or nil
+		)
+	}
+end
+
+return FilterButtonRow


Reviews:
user: login: hjpalpha
body: overall lgtm
the makeButton suggestion is just personal pref
the mention of the change (fa vs unicode) is something rath will have to decide ;)
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-03T06:43:10Z
user: login: ElectricalBoy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T09:07:19Z
user: login: hjpalpha
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T10:03:59Z
user: login: ElectricalBoy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T16:52:28Z
user: login: Rathoz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T08:49:26Z
user: login: Rathoz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T13:28:34Z
user: login: ElectricalBoy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T14:57:04Z
user: login: hjpalpha
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T17:07:32Z
user: login: Rathoz
body: LGTM
Can you manually deploy the Widgets first? Otherwise the main pages will break during the deploy 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-05T09:59:39Z
user: login: hjpalpha
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-05T10:19:31Z

Review comments:
diff_hunk: @@ -79,70 +80,78 @@ function FilterButtons._loadCategories(category)
 end
 
 ---@param category FilterButtonCategory
----@return Html
-function FilterButtons.getButtonRow(category)
-	local buttons = mw.html.create('div')
-		:addClass('filter-buttons')
-		:attr('data-filter', 'data-filter')
-		:attr('data-filter-effect','fade')
-		:attr('data-filter-group', 'filterbuttons-' .. category.name)
-		:attr('data-filter-default-curated', category.featuredByDefault and 'true' or nil)
-		:tag('span')
-			:addClass('filter-button')
-			:addClass('filter-button-all')
-			:attr('data-filter-on', 'all')
-			:wikitext(I18n.translate('filterbuttons-all'))
-			:done()
-
-	local makeButton = function(value, text)
-		local button = mw.html.create('span')
-			:addClass('filter-button')
-			:attr('data-filter-on', value)
-			:wikitext(text)
-		if Table.includes(category.defaultItems, value) then
-			button:addClass('filter-button--active')
-		end
-		buttons:node(button)
-	end
-
-	if category.hasFeatured then
-		makeButton('curated', I18n.translate('filterbuttons-featured'))
-	end
+---@param value string?
+---@param text string?
+---@return WidgetHtml
+function FilterButtons._makeButton(category, value, text)
+	return Span{
+		classes = {
+			'filter-button',
+			Table.includes(category.defaultItems, value) and 'filter-button--active' or nil
+		},
+		attributes = { ['data-filter-on'] = value },
+		children = { text }
+	}
+end
 
+---@param category FilterButtonCategory
+---@return Widget
+function FilterButtons.getButtonRow(category)
 	local transformValueToText = category.transform or FnUtil.identity
 	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
path: components/filter_buttons/commons/filter_buttons.lua
user: login: hjpalpha
body: ```suggestion
	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
	local makeButton = FnUtil.curry(FilterButtons._makeButton, category)
```
created_at: 2025-03-03T06:36:49Z
author_association: COLLABORATOR
diff_hunk: @@ -79,70 +80,78 @@ function FilterButtons._loadCategories(category)
 end
 
 ---@param category FilterButtonCategory
----@return Html
-function FilterButtons.getButtonRow(category)
-	local buttons = mw.html.create('div')
-		:addClass('filter-buttons')
-		:attr('data-filter', 'data-filter')
-		:attr('data-filter-effect','fade')
-		:attr('data-filter-group', 'filterbuttons-' .. category.name)
-		:attr('data-filter-default-curated', category.featuredByDefault and 'true' or nil)
-		:tag('span')
-			:addClass('filter-button')
-			:addClass('filter-button-all')
-			:attr('data-filter-on', 'all')
-			:wikitext(I18n.translate('filterbuttons-all'))
-			:done()
-
-	local makeButton = function(value, text)
-		local button = mw.html.create('span')
-			:addClass('filter-button')
-			:attr('data-filter-on', value)
-			:wikitext(text)
-		if Table.includes(category.defaultItems, value) then
-			button:addClass('filter-button--active')
-		end
-		buttons:node(button)
-	end
-
-	if category.hasFeatured then
-		makeButton('curated', I18n.translate('filterbuttons-featured'))
-	end
+---@param value string?
+---@param text string?
+---@return WidgetHtml
+function FilterButtons._makeButton(category, value, text)
+	return Span{
+		classes = {
+			'filter-button',
+			Table.includes(category.defaultItems, value) and 'filter-button--active' or nil
+		},
+		attributes = { ['data-filter-on'] = value },
+		children = { text }
+	}
+end
 
+---@param category FilterButtonCategory
+---@return Widget
+function FilterButtons.getButtonRow(category)
 	local transformValueToText = category.transform or FnUtil.identity
 	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
-	for _, value in ipairs(category.items or {}) do
-		local text = transformValueToText(value)
-		local filterValue = itemToPropertyValues(value) or value
-		makeButton(filterValue, text)
-	end
 
-	if String.isNotEmpty(category.expandKey) then
-		local dropdownButton = mw.html.create('div')
-			:addClass('filter-buttons')
-			:attr('data-filter', 'data-filter')
-			:attr('data-filter-effect','fade')
-			:attr('data-filter-group', 'tournaments-list-dropdown-' .. category.expandKey)
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:addClass('filter-button-dropdown')
-				:attr('data-filter-on', 'dropdown-' .. category.expandKey)
-				:wikitext(DROPDOWN_ARROW))
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:css('display','none')
-				:attr('data-filter-on', 'all'))
-
-		buttons:node(dropdownButton)
-	end
+	local buttons = Div{
+		classes = {'filter-buttons'},
+		attributes = {
+			['data-filter'] = 'data-filter',
+			['data-filter-effect'] = 'fade',
+			['data-filter-group'] = 'filterbuttons-' .. category.name,
+			['data-filter-default-curated'] = category.featuredByDefault and 'true' or nil,
+		},
+		children = WidgetUtil.collect(
+			Span{
+				classes = { 'filter-button', 'filter-button-all' },
+				attributes = { ['data-filter-on'] = 'all' },
+				children = { I18n.translate('filterbuttons-all') }
+			},
+			category.hasFeatured and FilterButtons._makeButton(category, 'curated', I18n.translate('filterbuttons-featured')),
path: components/filter_buttons/commons/filter_buttons.lua
user: login: hjpalpha
body: ```suggestion
			category.hasFeatured and makeButton('curated', I18n.translate('filterbuttons-featured')),
```
created_at: 2025-03-03T06:37:16Z
author_association: COLLABORATOR
diff_hunk: @@ -79,70 +80,78 @@ function FilterButtons._loadCategories(category)
 end
 
 ---@param category FilterButtonCategory
----@return Html
-function FilterButtons.getButtonRow(category)
-	local buttons = mw.html.create('div')
-		:addClass('filter-buttons')
-		:attr('data-filter', 'data-filter')
-		:attr('data-filter-effect','fade')
-		:attr('data-filter-group', 'filterbuttons-' .. category.name)
-		:attr('data-filter-default-curated', category.featuredByDefault and 'true' or nil)
-		:tag('span')
-			:addClass('filter-button')
-			:addClass('filter-button-all')
-			:attr('data-filter-on', 'all')
-			:wikitext(I18n.translate('filterbuttons-all'))
-			:done()
-
-	local makeButton = function(value, text)
-		local button = mw.html.create('span')
-			:addClass('filter-button')
-			:attr('data-filter-on', value)
-			:wikitext(text)
-		if Table.includes(category.defaultItems, value) then
-			button:addClass('filter-button--active')
-		end
-		buttons:node(button)
-	end
-
-	if category.hasFeatured then
-		makeButton('curated', I18n.translate('filterbuttons-featured'))
-	end
+---@param value string?
+---@param text string?
+---@return WidgetHtml
+function FilterButtons._makeButton(category, value, text)
+	return Span{
+		classes = {
+			'filter-button',
+			Table.includes(category.defaultItems, value) and 'filter-button--active' or nil
+		},
+		attributes = { ['data-filter-on'] = value },
+		children = { text }
+	}
+end
 
+---@param category FilterButtonCategory
+---@return Widget
+function FilterButtons.getButtonRow(category)
 	local transformValueToText = category.transform or FnUtil.identity
 	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
-	for _, value in ipairs(category.items or {}) do
-		local text = transformValueToText(value)
-		local filterValue = itemToPropertyValues(value) or value
-		makeButton(filterValue, text)
-	end
 
-	if String.isNotEmpty(category.expandKey) then
-		local dropdownButton = mw.html.create('div')
-			:addClass('filter-buttons')
-			:attr('data-filter', 'data-filter')
-			:attr('data-filter-effect','fade')
-			:attr('data-filter-group', 'tournaments-list-dropdown-' .. category.expandKey)
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:addClass('filter-button-dropdown')
-				:attr('data-filter-on', 'dropdown-' .. category.expandKey)
-				:wikitext(DROPDOWN_ARROW))
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:css('display','none')
-				:attr('data-filter-on', 'all'))
-
-		buttons:node(dropdownButton)
-	end
+	local buttons = Div{
+		classes = {'filter-buttons'},
+		attributes = {
+			['data-filter'] = 'data-filter',
+			['data-filter-effect'] = 'fade',
+			['data-filter-group'] = 'filterbuttons-' .. category.name,
+			['data-filter-default-curated'] = category.featuredByDefault and 'true' or nil,
+		},
+		children = WidgetUtil.collect(
+			Span{
+				classes = { 'filter-button', 'filter-button-all' },
+				attributes = { ['data-filter-on'] = 'all' },
+				children = { I18n.translate('filterbuttons-all') }
+			},
+			category.hasFeatured and FilterButtons._makeButton(category, 'curated', I18n.translate('filterbuttons-featured')),
+			Array.map(category.items or {}, function (value)
+				local text = transformValueToText(value)
+				local filterValue = itemToPropertyValues(value) or value
+				return FilterButtons._makeButton(category, filterValue, text)
path: components/filter_buttons/commons/filter_buttons.lua
user: login: hjpalpha
body: ```suggestion
				return makeButton(filterValue, text)
```
created_at: 2025-03-03T06:37:47Z
author_association: COLLABORATOR
diff_hunk: @@ -79,70 +80,78 @@ function FilterButtons._loadCategories(category)
 end
 
 ---@param category FilterButtonCategory
----@return Html
-function FilterButtons.getButtonRow(category)
-	local buttons = mw.html.create('div')
-		:addClass('filter-buttons')
-		:attr('data-filter', 'data-filter')
-		:attr('data-filter-effect','fade')
-		:attr('data-filter-group', 'filterbuttons-' .. category.name)
-		:attr('data-filter-default-curated', category.featuredByDefault and 'true' or nil)
-		:tag('span')
-			:addClass('filter-button')
-			:addClass('filter-button-all')
-			:attr('data-filter-on', 'all')
-			:wikitext(I18n.translate('filterbuttons-all'))
-			:done()
-
-	local makeButton = function(value, text)
-		local button = mw.html.create('span')
-			:addClass('filter-button')
-			:attr('data-filter-on', value)
-			:wikitext(text)
-		if Table.includes(category.defaultItems, value) then
-			button:addClass('filter-button--active')
-		end
-		buttons:node(button)
-	end
-
-	if category.hasFeatured then
-		makeButton('curated', I18n.translate('filterbuttons-featured'))
-	end
+---@param value string?
+---@param text string?
+---@return WidgetHtml
+function FilterButtons._makeButton(category, value, text)
+	return Span{
+		classes = {
+			'filter-button',
+			Table.includes(category.defaultItems, value) and 'filter-button--active' or nil
+		},
+		attributes = { ['data-filter-on'] = value },
+		children = { text }
+	}
+end
 
+---@param category FilterButtonCategory
+---@return Widget
+function FilterButtons.getButtonRow(category)
 	local transformValueToText = category.transform or FnUtil.identity
 	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
-	for _, value in ipairs(category.items or {}) do
-		local text = transformValueToText(value)
-		local filterValue = itemToPropertyValues(value) or value
-		makeButton(filterValue, text)
-	end
 
-	if String.isNotEmpty(category.expandKey) then
-		local dropdownButton = mw.html.create('div')
-			:addClass('filter-buttons')
-			:attr('data-filter', 'data-filter')
-			:attr('data-filter-effect','fade')
-			:attr('data-filter-group', 'tournaments-list-dropdown-' .. category.expandKey)
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:addClass('filter-button-dropdown')
-				:attr('data-filter-on', 'dropdown-' .. category.expandKey)
-				:wikitext(DROPDOWN_ARROW))
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:css('display','none')
-				:attr('data-filter-on', 'all'))
-
-		buttons:node(dropdownButton)
-	end
+	local buttons = Div{
+		classes = {'filter-buttons'},
+		attributes = {
+			['data-filter'] = 'data-filter',
+			['data-filter-effect'] = 'fade',
+			['data-filter-group'] = 'filterbuttons-' .. category.name,
+			['data-filter-default-curated'] = category.featuredByDefault and 'true' or nil,
+		},
+		children = WidgetUtil.collect(
+			Span{
+				classes = { 'filter-button', 'filter-button-all' },
+				attributes = { ['data-filter-on'] = 'all' },
+				children = { I18n.translate('filterbuttons-all') }
+			},
+			category.hasFeatured and FilterButtons._makeButton(category, 'curated', I18n.translate('filterbuttons-featured')),
+			Array.map(category.items or {}, function (value)
+				local text = transformValueToText(value)
+				local filterValue = itemToPropertyValues(value) or value
+				return FilterButtons._makeButton(category, filterValue, text)
+			end),
+			String.isNotEmpty(category.expandKey) and Div{
+				classes = { 'filter-buttons' },
+				attributes = {
+					['data-filter'] = 'data-filter',
+					['data-filter-effect'] ='fade',
+					['data-filter-group'] = 'tournaments-list-dropdown-' .. category.expandKey
+				},
+				children = {
+					Span{
+						classes = { 'filter-button', 'filter-button-dropdown' },
+						attributes = { ['data-filter-on'] = 'dropdown-' .. category.expandKey },
+						children = { IconFa{iconName = 'expand'} }
path: components/filter_buttons/commons/filter_buttons.lua
user: login: hjpalpha
body: fwiw unisg FontAwesome Icon instead of `'&#8203;▼&#8203;'` is a change to live
created_at: 2025-03-03T06:41:35Z
author_association: COLLABORATOR
diff_hunk: @@ -79,70 +80,78 @@ function FilterButtons._loadCategories(category)
 end
 
 ---@param category FilterButtonCategory
----@return Html
-function FilterButtons.getButtonRow(category)
-	local buttons = mw.html.create('div')
-		:addClass('filter-buttons')
-		:attr('data-filter', 'data-filter')
-		:attr('data-filter-effect','fade')
-		:attr('data-filter-group', 'filterbuttons-' .. category.name)
-		:attr('data-filter-default-curated', category.featuredByDefault and 'true' or nil)
-		:tag('span')
-			:addClass('filter-button')
-			:addClass('filter-button-all')
-			:attr('data-filter-on', 'all')
-			:wikitext(I18n.translate('filterbuttons-all'))
-			:done()
-
-	local makeButton = function(value, text)
-		local button = mw.html.create('span')
-			:addClass('filter-button')
-			:attr('data-filter-on', value)
-			:wikitext(text)
-		if Table.includes(category.defaultItems, value) then
-			button:addClass('filter-button--active')
-		end
-		buttons:node(button)
-	end
-
-	if category.hasFeatured then
-		makeButton('curated', I18n.translate('filterbuttons-featured'))
-	end
+---@param value string?
+---@param text string?
+---@return WidgetHtml
+function FilterButtons._makeButton(category, value, text)
+	return Span{
+		classes = {
+			'filter-button',
+			Table.includes(category.defaultItems, value) and 'filter-button--active' or nil
+		},
+		attributes = { ['data-filter-on'] = value },
+		children = { text }
+	}
+end
 
+---@param category FilterButtonCategory
+---@return Widget
+function FilterButtons.getButtonRow(category)
 	local transformValueToText = category.transform or FnUtil.identity
 	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
-	for _, value in ipairs(category.items or {}) do
-		local text = transformValueToText(value)
-		local filterValue = itemToPropertyValues(value) or value
-		makeButton(filterValue, text)
-	end
 
-	if String.isNotEmpty(category.expandKey) then
-		local dropdownButton = mw.html.create('div')
-			:addClass('filter-buttons')
-			:attr('data-filter', 'data-filter')
-			:attr('data-filter-effect','fade')
-			:attr('data-filter-group', 'tournaments-list-dropdown-' .. category.expandKey)
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:addClass('filter-button-dropdown')
-				:attr('data-filter-on', 'dropdown-' .. category.expandKey)
-				:wikitext(DROPDOWN_ARROW))
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:css('display','none')
-				:attr('data-filter-on', 'all'))
-
-		buttons:node(dropdownButton)
-	end
+	local buttons = Div{
+		classes = {'filter-buttons'},
+		attributes = {
+			['data-filter'] = 'data-filter',
+			['data-filter-effect'] = 'fade',
+			['data-filter-group'] = 'filterbuttons-' .. category.name,
+			['data-filter-default-curated'] = category.featuredByDefault and 'true' or nil,
+		},
+		children = WidgetUtil.collect(
+			Span{
+				classes = { 'filter-button', 'filter-button-all' },
+				attributes = { ['data-filter-on'] = 'all' },
+				children = { I18n.translate('filterbuttons-all') }
+			},
+			category.hasFeatured and FilterButtons._makeButton(category, 'curated', I18n.translate('filterbuttons-featured')),
+			Array.map(category.items or {}, function (value)
+				local text = transformValueToText(value)
+				local filterValue = itemToPropertyValues(value) or value
+				return FilterButtons._makeButton(category, filterValue, text)
+			end),
+			String.isNotEmpty(category.expandKey) and Div{
+				classes = { 'filter-buttons' },
+				attributes = {
+					['data-filter'] = 'data-filter',
+					['data-filter-effect'] ='fade',
+					['data-filter-group'] = 'tournaments-list-dropdown-' .. category.expandKey
+				},
+				children = {
+					Span{
+						classes = { 'filter-button', 'filter-button-dropdown' },
+						attributes = { ['data-filter-on'] = 'dropdown-' .. category.expandKey },
+						children = { IconFa{iconName = 'expand'} }
path: components/filter_buttons/commons/filter_buttons.lua
user: login: ElectricalBoy
body: I am aware; thought it could be something that could also be cleaned up while I'm at it anyway
I favor using FontAwesome icon but don't mind keeping the unicode character either
created_at: 2025-03-03T09:07:19Z
author_association: COLLABORATOR
diff_hunk: @@ -79,70 +80,78 @@ function FilterButtons._loadCategories(category)
 end
 
 ---@param category FilterButtonCategory
----@return Html
-function FilterButtons.getButtonRow(category)
-	local buttons = mw.html.create('div')
-		:addClass('filter-buttons')
-		:attr('data-filter', 'data-filter')
-		:attr('data-filter-effect','fade')
-		:attr('data-filter-group', 'filterbuttons-' .. category.name)
-		:attr('data-filter-default-curated', category.featuredByDefault and 'true' or nil)
-		:tag('span')
-			:addClass('filter-button')
-			:addClass('filter-button-all')
-			:attr('data-filter-on', 'all')
-			:wikitext(I18n.translate('filterbuttons-all'))
-			:done()
-
-	local makeButton = function(value, text)
-		local button = mw.html.create('span')
-			:addClass('filter-button')
-			:attr('data-filter-on', value)
-			:wikitext(text)
-		if Table.includes(category.defaultItems, value) then
-			button:addClass('filter-button--active')
-		end
-		buttons:node(button)
-	end
-
-	if category.hasFeatured then
-		makeButton('curated', I18n.translate('filterbuttons-featured'))
-	end
+---@param value string?
+---@param text string?
+---@return WidgetHtml
+function FilterButtons._makeButton(category, value, text)
+	return Span{
+		classes = {
+			'filter-button',
+			Table.includes(category.defaultItems, value) and 'filter-button--active' or nil
+		},
+		attributes = { ['data-filter-on'] = value },
+		children = { text }
+	}
+end
 
+---@param category FilterButtonCategory
+---@return Widget
+function FilterButtons.getButtonRow(category)
 	local transformValueToText = category.transform or FnUtil.identity
 	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
-	for _, value in ipairs(category.items or {}) do
-		local text = transformValueToText(value)
-		local filterValue = itemToPropertyValues(value) or value
-		makeButton(filterValue, text)
-	end
 
-	if String.isNotEmpty(category.expandKey) then
-		local dropdownButton = mw.html.create('div')
-			:addClass('filter-buttons')
-			:attr('data-filter', 'data-filter')
-			:attr('data-filter-effect','fade')
-			:attr('data-filter-group', 'tournaments-list-dropdown-' .. category.expandKey)
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:addClass('filter-button-dropdown')
-				:attr('data-filter-on', 'dropdown-' .. category.expandKey)
-				:wikitext(DROPDOWN_ARROW))
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:css('display','none')
-				:attr('data-filter-on', 'all'))
-
-		buttons:node(dropdownButton)
-	end
+	local buttons = Div{
+		classes = {'filter-buttons'},
+		attributes = {
+			['data-filter'] = 'data-filter',
+			['data-filter-effect'] = 'fade',
+			['data-filter-group'] = 'filterbuttons-' .. category.name,
+			['data-filter-default-curated'] = category.featuredByDefault and 'true' or nil,
+		},
+		children = WidgetUtil.collect(
+			Span{
+				classes = { 'filter-button', 'filter-button-all' },
+				attributes = { ['data-filter-on'] = 'all' },
+				children = { I18n.translate('filterbuttons-all') }
+			},
+			category.hasFeatured and FilterButtons._makeButton(category, 'curated', I18n.translate('filterbuttons-featured')),
+			Array.map(category.items or {}, function (value)
+				local text = transformValueToText(value)
+				local filterValue = itemToPropertyValues(value) or value
+				return FilterButtons._makeButton(category, filterValue, text)
+			end),
+			String.isNotEmpty(category.expandKey) and Div{
+				classes = { 'filter-buttons' },
+				attributes = {
+					['data-filter'] = 'data-filter',
+					['data-filter-effect'] ='fade',
+					['data-filter-group'] = 'tournaments-list-dropdown-' .. category.expandKey
+				},
+				children = {
+					Span{
+						classes = { 'filter-button', 'filter-button-dropdown' },
+						attributes = { ['data-filter-on'] = 'dropdown-' .. category.expandKey },
+						children = { IconFa{iconName = 'expand'} }
path: components/filter_buttons/commons/filter_buttons.lua
user: login: hjpalpha
body: i don't mind either, just mentioning that it is a change^^
rath will have to decide :P

btw did you check how this change behaves in both darkmode and lightmode?
created_at: 2025-03-03T10:03:59Z
author_association: COLLABORATOR
diff_hunk: @@ -79,70 +80,78 @@ function FilterButtons._loadCategories(category)
 end
 
 ---@param category FilterButtonCategory
----@return Html
-function FilterButtons.getButtonRow(category)
-	local buttons = mw.html.create('div')
-		:addClass('filter-buttons')
-		:attr('data-filter', 'data-filter')
-		:attr('data-filter-effect','fade')
-		:attr('data-filter-group', 'filterbuttons-' .. category.name)
-		:attr('data-filter-default-curated', category.featuredByDefault and 'true' or nil)
-		:tag('span')
-			:addClass('filter-button')
-			:addClass('filter-button-all')
-			:attr('data-filter-on', 'all')
-			:wikitext(I18n.translate('filterbuttons-all'))
-			:done()
-
-	local makeButton = function(value, text)
-		local button = mw.html.create('span')
-			:addClass('filter-button')
-			:attr('data-filter-on', value)
-			:wikitext(text)
-		if Table.includes(category.defaultItems, value) then
-			button:addClass('filter-button--active')
-		end
-		buttons:node(button)
-	end
-
-	if category.hasFeatured then
-		makeButton('curated', I18n.translate('filterbuttons-featured'))
-	end
+---@param value string?
+---@param text string?
+---@return WidgetHtml
+function FilterButtons._makeButton(category, value, text)
+	return Span{
+		classes = {
+			'filter-button',
+			Table.includes(category.defaultItems, value) and 'filter-button--active' or nil
+		},
+		attributes = { ['data-filter-on'] = value },
+		children = { text }
+	}
+end
 
+---@param category FilterButtonCategory
+---@return Widget
+function FilterButtons.getButtonRow(category)
 	local transformValueToText = category.transform or FnUtil.identity
 	local itemToPropertyValues = category.itemToPropertyValues or FnUtil.identity
-	for _, value in ipairs(category.items or {}) do
-		local text = transformValueToText(value)
-		local filterValue = itemToPropertyValues(value) or value
-		makeButton(filterValue, text)
-	end
 
-	if String.isNotEmpty(category.expandKey) then
-		local dropdownButton = mw.html.create('div')
-			:addClass('filter-buttons')
-			:attr('data-filter', 'data-filter')
-			:attr('data-filter-effect','fade')
-			:attr('data-filter-group', 'tournaments-list-dropdown-' .. category.expandKey)
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:addClass('filter-button-dropdown')
-				:attr('data-filter-on', 'dropdown-' .. category.expandKey)
-				:wikitext(DROPDOWN_ARROW))
-			:node(mw.html.create('span')
-				:addClass('filter-button')
-				:css('display','none')
-				:attr('data-filter-on', 'all'))
-
-		buttons:node(dropdownButton)
-	end
+	local buttons = Div{
+		classes = {'filter-buttons'},
+		attributes = {
+			['data-filter'] = 'data-filter',
+			['data-filter-effect'] = 'fade',
+			['data-filter-group'] = 'filterbuttons-' .. category.name,
+			['data-filter-default-curated'] = category.featuredByDefault and 'true' or nil,
+		},
+		children = WidgetUtil.collect(
+			Span{
+				classes = { 'filter-button', 'filter-button-all' },
+				attributes = { ['data-filter-on'] = 'all' },
+				children = { I18n.translate('filterbuttons-all') }
+			},
+			category.hasFeatured and FilterButtons._makeButton(category, 'curated', I18n.translate('filterbuttons-featured')),
+			Array.map(category.items or {}, function (value)
+				local text = transformValueToText(value)
+				local filterValue = itemToPropertyValues(value) or value
+				return FilterButtons._makeButton(category, filterValue, text)
+			end),
+			String.isNotEmpty(category.expandKey) and Div{
+				classes = { 'filter-buttons' },
+				attributes = {
+					['data-filter'] = 'data-filter',
+					['data-filter-effect'] ='fade',
+					['data-filter-group'] = 'tournaments-list-dropdown-' .. category.expandKey
+				},
+				children = {
+					Span{
+						classes = { 'filter-button', 'filter-button-dropdown' },
+						attributes = { ['data-filter-on'] = 'dropdown-' .. category.expandKey },
+						children = { IconFa{iconName = 'expand'} }
path: components/filter_buttons/commons/filter_buttons.lua
user: login: ElectricalBoy
body: behaves nicely in darkmode (I mainly use darkmode)

tested lightmode just now and it looks as good imo
created_at: 2025-03-03T16:52:28Z
author_association: COLLABORATOR
path: components/filter_buttons/commons/filter_buttons.lua
user: login: Rathoz
body: How about making it into a Widget as well :D?
created_at: 2025-03-04T08:49:26Z
author_association: COLLABORATOR
path: components/filter_buttons/commons/filter_buttons.lua
user: login: Rathoz
body: I meant moving the Filter Button into the Widgets folder & implementing the Widget interface
created_at: 2025-03-04T13:28:34Z
author_association: COLLABORATOR
path: components/filter_buttons/commons/filter_buttons.lua
user: login: ElectricalBoy
body: ohh that makes more sense...
something for me to look into tomorrow
created_at: 2025-03-04T14:57:04Z
author_association: COLLABORATOR
path: components/filter_buttons/commons/filter_buttons.lua
user: login: hjpalpha
body: see #5547 for a sugegstion for this (still to be tgested the time i wrote this comment)^^
created_at: 2025-03-04T17:07:32Z
author_association: COLLABORATOR

PR comments:
user: login: hjpalpha
created_at: 2025-03-03T06:44:12Z
body: do we have a wiki which uses filterbuttons but does not have new main page yet?
if so should be tested there too ;)
author_association: COLLABORATOR
user: login: ElectricalBoy
created_at: 2025-03-03T08:50:14Z
body: > do we have a wiki which uses filterbuttons but does not have new main page yet?
> if so should be tested there too ;)

I don't think there's such a wiki, but still worth double-checking :)

update: and no, all usages are either inside the new main page (incl. userspace tests) or in `[[Liquipedia:Matches]]` page (which I tested and confirmed working just now)
author_association: COLLABORATOR
user: login: Rathoz
created_at: 2025-03-04T08:48:23Z
body: > do we have a wiki which uses filterbuttons but does not have new main page yet? if so should be tested there too ;)

AFAIK, no, if there is, probably AoE
author_association: COLLABORATOR
user: login: ElectricalBoy
created_at: 2025-03-05T10:10:57Z
body: > Can you manually deploy the Widgets first? Otherwise the main pages will break during the deploy

https://liquipedia.net/commons/Module:Widget/FilterButtons
https://liquipedia.net/commons/Module:Widget/FilterButtons/Button
https://liquipedia.net/commons/Module:Widget/FilterButtons/ButtonRow

headers are intentionally omitted to let the bot put them in
author_association: COLLABORATOR