Extending jQuery UI Widgets for Enhanced User Interfaces
Creating compelling user experiences involves leveraging tools that streamline development while maintaining flexibility. jQuery UI fills gaps left by inconsistent browser implementations and provides a foundation for building robust, themeable interfaces. This guide explores practical techniques for extending core jQuery UI widgets—Accordion, Autocomplete, and Button—to support advanced interactions like drag-and-drop, dynamic resizing, multi-source data handling, and animated transitions.
Enhancing the Accordion Widget
The Accordion widget organizes content into collapsible sections. While functional out of the box, it can be extended to support richer interactions.
Tab Navigation Between Sections
By default, Accordions use arrow keys for navigation. To enable Tab and Shift+Tab navigation between headers:
(function($, undefined) {
$.widget("custom.accordion", $.ui.accordion, {
_create: function() {
this._super("_create");
this._on(this.headers, { keydown: "_handleTabKey" });
},
_handleTabKey: function(event) {
if (event.altKey || event.ctrlKey || event.keyCode !== $.ui.keyCode.TAB) return;
const headers = this.headers,
currentIndex = headers.index(event.target),
isBackward = event.shiftKey,
newIndex = isBackward ? currentIndex - 1 : currentIndex + 1;
if (newIndex >= 0 && newIndex < headers.length) {
$(event.target).attr("tabindex", -1);
const newHeader = headers.eq(newIndex);
newHeader.attr("tabindex", 0).focus();
event.preventDefault();
}
}
});
})(jQuery);
Dynamic Height Adjustment
When content changes dynamically, override the refresh() method to switch to heightStyle: "content" and clear fixed heights:
$.widget("custom.accordion", $.ui.accordion, {
refresh: function() {
this._super("refresh");
if (this.options.heightStyle === "content") {
this.headers.next().css("height", "");
}
}
});
Resizable Content Panels
Integrate jQuery UI's Resizable interaction to let users adjust panel heights:
$.widget("custom.accordion", $.ui.accordion, {
options: { resizable: true },
_create: function() {
this._super("_create");
if (this.options.resizable) {
this.headers.next()
.resizable({ handles: "s" })
.css("overflow", "hidden");
}
},
_destroy: function() {
this._super("_destroy");
if (this.options.resizable) {
this.headers.next()
.resizable("destroy")
.css("overflow", "");
}
}
});
Drag-and-Drop Between Accordions
Enible transferring sections between two Accordions using Draggable and Sortable:
$.widget("custom.accordion", $.ui.accordion, {
options: { target: null, accept: null },
_create: function() {
this._super("_create");
if (this.options.target) this._makeDraggable();
if (this.options.accept) this._makeDroppable();
},
_makeDraggable: function() {
this.headers.each(function() {
$(this).next().addBack().wrapAll("<div>");
});
this.element.find("> div").draggable({
handle: "h3",
helper: "clone",
connectToSortable: this.options.target
});
},
_makeDroppable: function() {
this.headers.each(function() {
$(this).next().addBack().wrapAll("<div>");
});
this.element.sortable({
stop: (event, ui) => {
if (!ui.item.hasClass("ui-draggable")) return;
// Clone cleanup and event rebinding logic here
}
});
}
});
Advanced Autocomplete Implementations
The Autocomplete widget enhances input fields with suggestion lists. Key extensions include:
Styling Input Fields
Apply jQuery UI theme classes to the input element:
$.widget("custom.autocomplete", $.ui.autocomplete, {
_create: function() {
this._super("_create");
this.element.addClass("ui-widget ui-widget-content ui-corner-all");
}
});
Multiple Data Sources
Combine arrays from different sources into a single suggestion list:
$.widget("custom.autocomplete", $.ui.autocomplete, {
options: { sources: [] },
_create: function() {
if (this.options.sources.length) {
this.options.source = (request, response) => {
const allItems = [].concat(...this.options.sources);
response($.ui.autocomplete.filter(allItems, request.term));
};
}
this._super("_create");
}
});
Categorized Results
Display suggestions grouped by category with custom rendering:
$.widget("custom.autocomplete", $.ui.autocomplete, {
_renderMenu: function(ul, items) {
let currentCategory = "";
items.sort((a, b) => a.cat.localeCompare(b.cat));
items.forEach(item => {
if (item.cat !== currentCategory) {
ul.append($("<li class='ui-autocomplete-category'>").text(item.cat));
currentCategory = item.cat;
}
this._renderItem(ul, item);
});
},
_renderItem: function(ul, item) {
return $("<li>").append($("<a>")
.append($("<div>").text(item.label))
.append($("<small>").text(item.desc))
).appendTo(ul);
}
});
Customizing Button Widgets
Buttons can be enhanced for better usability and visual consistency.
Consistent Button Widths
Make all buttons in a group match the widest button's width:
$.widget("custom.button", $.ui.button, {
options: { matchWidth: false },
refresh: function() {
this._super("refresh");
if (!this.options.matchWidth) return;
const siblings = this.element.siblings(":custom-button").addBack(),
texts = siblings.children(".ui-button-text"),
maxWidth = Math.max(...texts.map((i, el) => $(el).width()));
texts.width(maxWidth);
}
});
Animated Hover States
Add smooth transitions to hover effects:
$.widget("custom.button", $.ui.button, {
options: { animateHover: false },
_create: function() {
this._super("_create");
if (this.options.animateHover) {
this._off(this.element, "mouseenter mouseleave");
this._on({
mouseenter: () => this.element.addClass("ui-state-hover", 200),
mouseleave: () => this.element.removeClass("ui-state-hover", 100)
});
}
}
});
Toggle Icons Visibility
Prseerve icon configurations when toggling visibility:
$.widget("custom.button", $.ui.button, {
options: { icon: true },
_hiddenIcons: {},
_setOption: function(key, value) {
if (key !== "icon") return this._superApply(arguments);
if (!value && !$.isEmptyObject(this.options.icons)) {
this._hiddenIcons = this.options.icons;
this._super("icons", {});
} else if (value && $.isEmptyObject(this.options.icons)) {
this._super("icons", this._hiddenIcons);
}
}
});