feat: add new horizontal tabs navigation user experience (#18310)

* feat: Add none scrollable horizontal tab user experience

Introduces `isHorizontal` and `isScrollable` properties to the Tabs component, allowing configuration of tab orientation and scrollability. Updates Blade, JS, and Alpine logic to support scrollable tabs with dropdown overflow handling and responsive behavior.

* style: run composer cs

* Refactor Tabs component and update related styles

Refactored the Tabs component across PHP, Blade, and JS implementations to improve maintainability and consistency. Updated related CSS for dropdown list items to enhance UI appearance. These changes ensure better integration and user experience for tabbed interfaces.

* Refactor tabs dropdown logic for improved visibility

Replaces the scrollable tabs logic with a more robust dropdown visibility calculation. Updates Alpine component, Blade template, and supporting JS to use new withinDropdownMounted and isDropDownButtonVisible properties, ensuring correct tab visibility and dropdown trigger behavior. Removes unused tabIndex and related code, and cleans up CSS.

* chore: fix code style

* chore: fix code style

* Add documentation for horizontal tabs usage

Added a new section explaining how to use horizontal tabs, including details about the default scrollable behavior and the `horizontal()` method with its `scrollable` parameter.

* Document horizontal tabs overflow behavior

Added explanation for how non-scrollable horizontal tabs handle overflow by grouping excess tabs into a dropdown when space is limited.

* chore: fix code style

* clean up

* Update tabs.blade.php

* more robust calculation

* chore: fix code style

* Revert "chore: fix code style"

This reverts commit 3acc6fe781.

* refactor and support badges

* refactor php

* Update tabs.blade.php

* Update tabs.js

* Update tabs.blade.php

* screenshots

---------

Co-authored-by: Abdulmajeed-Jamaan <Abdulmajeed-Jamaan@users.noreply.github.com>
Co-authored-by: Dan Harrin <git@danharrin.com>
Co-authored-by: danharrin <danharrin@users.noreply.github.com>
This commit is contained in:
Abdulmajeed Jamaan
2025-12-04 10:48:51 +00:00
committed by GitHub
co-authored by Abdulmajeed-Jamaan Dan Harrin danharrin
parent 8f9380a85e
commit cdafd6e52b
15 changed files with 1796 additions and 1231 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ APP_NAME=Laravel
APP_ENV=ci
APP_KEY=base64:/RjsPfTKUG/cV+SQjSOfCfgFPZ85Zmwywiz5yevEZ0s=
APP_DEBUG=true
APP_URL=http://127.0.0.1::8000
APP_URL=http://127.0.0.1:8000
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
@@ -284,6 +284,28 @@ class LayoutDemo extends Component implements HasActions, HasSchemas
])
->vertical(),
]),
Group::make()
->id('tabsNotScrollable')
->extraAttributes([
'class' => 'p-16 max-w-xl',
])
->schema([
Tabs::make('Tabs')
->statePath('tabsNotScrollable')
->schema([
Tab::make('Tab 1')
->schema([
TextInput::make('field'),
]),
Tab::make('Tab 2'),
Tab::make('Tab 3'),
Tab::make('Tab 4'),
Tab::make('Tab 5'),
Tab::make('Tab 6'),
Tab::make('Tab 7'),
])
->scrollable(false),
]),
Group::make()
->id('wizard')
->extraAttributes([
+882 -691
View File
File diff suppressed because it is too large Load Diff
+525 -493
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+9
View File
@@ -1610,6 +1610,15 @@ export default {
deviceScaleFactor: 3,
},
},
'schemas/layout/tabs/not-scrollable': {
url: 'schemas/layout',
selector: '#tabsNotScrollable',
viewport: {
width: 1920,
height: 640,
deviceScaleFactor: 3,
},
},
'schemas/layout/wizard/simple': {
url: 'schemas/layout',
selector: '#wizard',
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
function u({activeTab:a,isTabPersistedInQueryString:e,livewireId:h,tab:o,tabQueryStringKey:s}){return{tab:o,init(){let t=this.getTabs(),i=new URLSearchParams(window.location.search);e&&i.has(s)&&t.includes(i.get(s))&&(this.tab=i.get(s)),this.$watch("tab",()=>this.updateQueryString()),(!this.tab||!t.includes(this.tab))&&(this.tab=t[a-1]),Livewire.hook("commit",({component:r,commit:f,succeed:c,fail:l,respond:b})=>{c(({snapshot:d,effect:m})=>{this.$nextTick(()=>{if(r.id!==h)return;let n=this.getTabs();n.includes(this.tab)||(this.tab=n[a-1]??this.tab)})})})},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!e)return;let t=new URL(window.location.href);t.searchParams.set(s,this.tab),history.replaceState(null,document.title,t.toString())}}}export{u as default};
function I({activeTab:w,isScrollable:f,isTabPersistedInQueryString:m,livewireId:g,tab:T,tabQueryStringKey:c}){return{boundResizeHandler:null,isScrollable:f,resizeDebounceTimer:null,tab:T,withinDropdownIndex:null,withinDropdownMounted:!1,init(){let t=this.getTabs(),e=new URLSearchParams(window.location.search);m&&e.has(c)&&t.includes(e.get(c))&&(this.tab=e.get(c)),this.$watch("tab",()=>this.updateQueryString()),(!this.tab||!t.includes(this.tab))&&(this.tab=t[w-1]),Livewire.hook("commit",({component:n,commit:d,succeed:r,fail:h,respond:u})=>{r(({snapshot:p,effect:i})=>{this.$nextTick(()=>{if(n.id!==g)return;let s=this.getTabs();s.includes(this.tab)||(this.tab=s[w-1]??this.tab)})})}),f||(this.boundResizeHandler=this.debouncedUpdateTabsWithinDropdown.bind(this),window.addEventListener("resize",this.boundResizeHandler),this.updateTabsWithinDropdown())},calculateAvailableWidth(t){let e=window.getComputedStyle(t);return Math.floor(t.clientWidth)-Math.ceil(parseFloat(e.paddingLeft))*2},calculateContainerGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap))},calculateDropdownIconWidth(t){let e=t.querySelector(".fi-icon");return Math.ceil(e.clientWidth)},calculateTabItemGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap)||8)},calculateTabItemPadding(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.paddingLeft))+Math.ceil(parseFloat(e.paddingRight))},findOverflowIndex(t,e,n,d,r,h){let u=t.map(i=>Math.ceil(i.clientWidth)),p=t.map(i=>{let s=i.querySelector(".fi-tabs-item-label"),a=i.querySelector(".fi-badge"),o=Math.ceil(s.clientWidth),l=a?Math.ceil(a.clientWidth):0;return{label:o,badge:l,total:o+(l>0?d+l:0)}});for(let i=0;i<t.length;i++){let s=u.slice(0,i+1).reduce((b,y)=>b+y,0),a=i*n,o=p.slice(i+1),l=o.length>0,W=l?Math.max(...o.map(b=>b.total)):0,D=l?r+W+d+h+n:0;if(s+a+D>e)return i}return-1},get isDropdownButtonVisible(){return this.withinDropdownMounted?this.withinDropdownIndex===null?!1:this.getTabs().findIndex(e=>e===this.tab)<this.withinDropdownIndex:!0},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!m)return;let t=new URL(window.location.href);t.searchParams.set(c,this.tab),history.replaceState(null,document.title,t.toString())},debouncedUpdateTabsWithinDropdown(){clearTimeout(this.resizeDebounceTimer),this.resizeDebounceTimer=setTimeout(()=>this.updateTabsWithinDropdown(),150)},async updateTabsWithinDropdown(){this.withinDropdownIndex=null,this.withinDropdownMounted=!1,await this.$nextTick();let t=this.$el.querySelector(".fi-tabs"),e=t.querySelector(".fi-tabs-item:last-child"),n=Array.from(t.children).slice(0,-1),d=n.map(a=>a.style.display);n.forEach(a=>a.style.display=""),t.offsetHeight;let r=this.calculateAvailableWidth(t),h=this.calculateContainerGap(t),u=this.calculateDropdownIconWidth(e),p=this.calculateTabItemGap(n[0]),i=this.calculateTabItemPadding(n[0]),s=this.findOverflowIndex(n,r,h,p,i,u);n.forEach((a,o)=>a.style.display=d[o]),s!==-1&&(this.withinDropdownIndex=s),this.withinDropdownMounted=!0},destroy(){this.boundResizeHandler&&window.removeEventListener("resize",this.boundResizeHandler),clearTimeout(this.resizeDebounceTimer)}}}export{I as default};
+20
View File
@@ -177,6 +177,26 @@ Tabs::make('Tabs')
<UtilityInjection set="schemaComponents" version="4.x">As well as allowing a static value, the `columns()` method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.</UtilityInjection>
## Disabling scrollable tabs
Tabs are rendered horizontally by default, and are scrollable when they exceed the available width.
You may disable scrolling using the `scrollable(false)` method:
```php
use Filament\Schemas\Components\Tabs;
Tabs::make('Tabs')
->tabs([
// ...
])
->scrollable(false)
```
When tabs are not scrollable, the component automatically detects the available width. If not all tabs can fit, a dropdown button will appear. Any tabs that exceed the available width will be grouped inside this dropdown automatically.
<AutoScreenshot name="schemas/layout/tabs/not-scrollable" alt="Non-scrollable tabs with overflow dropdown" version="4.x" />
## Using vertical tabs
You can render the tabs vertically by using the `vertical()` method:
@@ -1,6 +1,10 @@
.fi-sc-tabs {
@apply flex flex-col;
& .fi-tabs.fi-invisible {
@apply invisible;
}
& .fi-sc-tabs-tab {
@apply outline-hidden;
@@ -1,16 +1,21 @@
export default function tabsSchemaComponent({
activeTab,
isScrollable,
isTabPersistedInQueryString,
livewireId,
tab,
tabQueryStringKey,
}) {
return {
boundResizeHandler: null,
isScrollable,
resizeDebounceTimer: null,
tab,
withinDropdownIndex: null,
withinDropdownMounted: false,
init() {
const tabs = this.getTabs()
const queryString = new URLSearchParams(window.location.search)
if (
@@ -45,14 +50,132 @@ export default function tabsSchemaComponent({
})
},
)
if (!isScrollable) {
this.boundResizeHandler =
this.debouncedUpdateTabsWithinDropdown.bind(this)
window.addEventListener('resize', this.boundResizeHandler)
this.updateTabsWithinDropdown()
}
},
calculateAvailableWidth(containerEl) {
const styles = window.getComputedStyle(containerEl)
return (
Math.floor(containerEl.clientWidth) -
Math.ceil(parseFloat(styles.paddingLeft)) * 2
)
},
calculateContainerGap(containerEl) {
const styles = window.getComputedStyle(containerEl)
return Math.ceil(parseFloat(styles.columnGap))
},
calculateDropdownIconWidth(triggerEl) {
const iconEl = triggerEl.querySelector('.fi-icon')
return Math.ceil(iconEl.clientWidth)
},
calculateTabItemGap(tabEl) {
const styles = window.getComputedStyle(tabEl)
return Math.ceil(parseFloat(styles.columnGap) || 8)
},
calculateTabItemPadding(tabEl) {
const styles = window.getComputedStyle(tabEl)
return (
Math.ceil(parseFloat(styles.paddingLeft)) +
Math.ceil(parseFloat(styles.paddingRight))
)
},
findOverflowIndex(
tabElements,
availableWidth,
containerGap,
tabItemGap,
tabItemPadding,
dropdownIconWidth,
) {
const tabWidths = tabElements.map((el) => Math.ceil(el.clientWidth))
const tabContentWidths = tabElements.map((el) => {
const labelEl = el.querySelector('.fi-tabs-item-label')
const badgeEl = el.querySelector('.fi-badge')
const labelWidth = Math.ceil(labelEl.clientWidth)
const badgeWidth = badgeEl ? Math.ceil(badgeEl.clientWidth) : 0
return {
label: labelWidth,
badge: badgeWidth,
total:
labelWidth +
(badgeWidth > 0 ? tabItemGap + badgeWidth : 0),
}
})
for (let i = 0; i < tabElements.length; i++) {
const visibleTabsWidth = tabWidths
.slice(0, i + 1)
.reduce((sum, w) => sum + w, 0)
const gapsBetweenVisibleTabs = i * containerGap
const collapsedContents = tabContentWidths.slice(i + 1)
const hasCollapsedTabs = collapsedContents.length > 0
const widestCollapsedContent = hasCollapsedTabs
? Math.max(...collapsedContents.map((c) => c.total))
: 0
const triggerWidth = hasCollapsedTabs
? tabItemPadding +
widestCollapsedContent +
tabItemGap +
dropdownIconWidth +
containerGap
: 0
const totalWidth =
visibleTabsWidth + gapsBetweenVisibleTabs + triggerWidth
if (totalWidth > availableWidth) {
return i
}
}
return -1
},
get isDropdownButtonVisible() {
if (!this.withinDropdownMounted) {
return true
}
if (this.withinDropdownIndex === null) {
return false
}
const activeTabIndex = this.getTabs().findIndex(
(tab) => tab === this.tab,
)
return activeTabIndex < this.withinDropdownIndex
},
getTabs() {
if (!this.$refs.tabsData) {
return []
}
return JSON.parse(this.$refs.tabsData.value)
return this.$refs.tabsData
? JSON.parse(this.$refs.tabsData.value)
: []
},
updateQueryString() {
@@ -65,5 +188,68 @@ export default function tabsSchemaComponent({
history.replaceState(null, document.title, url.toString())
},
debouncedUpdateTabsWithinDropdown() {
clearTimeout(this.resizeDebounceTimer)
this.resizeDebounceTimer = setTimeout(
() => this.updateTabsWithinDropdown(),
150,
)
},
async updateTabsWithinDropdown() {
this.withinDropdownIndex = null
this.withinDropdownMounted = false
await this.$nextTick()
const containerEl = this.$el.querySelector('.fi-tabs')
const triggerEl = containerEl.querySelector(
'.fi-tabs-item:last-child',
)
const tabElements = Array.from(containerEl.children).slice(0, -1)
// Force all tabs visible for accurate measurement
const originalStyles = tabElements.map((el) => el.style.display)
tabElements.forEach((el) => (el.style.display = ''))
// Force reflow to ensure dimensions are calculated
containerEl.offsetHeight
const availableWidth = this.calculateAvailableWidth(containerEl)
const containerGap = this.calculateContainerGap(containerEl)
const dropdownIconWidth = this.calculateDropdownIconWidth(triggerEl)
const tabItemGap = this.calculateTabItemGap(tabElements[0])
const tabItemPadding = this.calculateTabItemPadding(tabElements[0])
const overflowIndex = this.findOverflowIndex(
tabElements,
availableWidth,
containerGap,
tabItemGap,
tabItemPadding,
dropdownIconWidth,
)
// Restore original display styles
tabElements.forEach(
(el, i) => (el.style.display = originalStyles[i]),
)
if (overflowIndex !== -1) {
this.withinDropdownIndex = overflowIndex
}
this.withinDropdownMounted = true
},
destroy() {
if (this.boundResizeHandler) {
window.removeEventListener('resize', this.boundResizeHandler)
}
clearTimeout(this.resizeDebounceTimer)
},
}
}
@@ -1,26 +1,57 @@
@php
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\View\SchemaIconAlias;
use Filament\Support\Icons\Heroicon;
$activeTab = $getActiveTab();
$id = $getId();
$isContained = $isContained();
$isScrollable = $isScrollable();
$isVertical = $isVertical();
$label = $getLabel();
$livewireProperty = $getLivewireProperty();
$renderHookScopes = $getRenderHookScopes();
$id = $getId();
$tabs = $getChildSchema()->getComponents();
$getTabVisibilityJs = function (Tab $tab, ?int $index = null, ?string $mode = null) use ($isScrollable): ?string {
$hiddenJs = $tab->getHiddenJs();
$visibleJs = $tab->getVisibleJs();
$baseJs = match ([filled($hiddenJs), filled($visibleJs)]) {
[true, true] => "(! ({$hiddenJs})) && ({$visibleJs})",
[true, false] => "! ({$hiddenJs})",
[false, true] => $visibleJs,
default => null,
};
if ($isScrollable || $index === null || $mode === null) {
return $baseJs;
}
$tabKey = $tab->getKey(isAbsolute: false);
$dropdownJs = match ($mode) {
'inline' => "(!withinDropdownMounted || withinDropdownIndex === null || {$index} < withinDropdownIndex)",
'trigger' => "(withinDropdownMounted && withinDropdownIndex !== null && {$index} >= withinDropdownIndex && '{$tabKey}' === tab)",
default => null,
};
return $baseJs ? "{$baseJs} && {$dropdownJs}" : $dropdownJs;
};
@endphp
@if (blank($livewireProperty))
<div
x-load
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('tabs', 'filament/schemas') }}"
x-data="tabsSchemaComponent({
activeTab: @js($activeTab),
isScrollable: @js($isScrollable),
isTabPersistedInQueryString: @js($isTabPersistedInQueryString()),
livewireId: @js($this->getId()),
tab: @if ($isTabPersisted() && filled($id)) $persist(null).as(@js($id)) @else @js(null) @endif,
tabQueryStringKey: @js($getTabQueryStringKey()),
})"
x-load
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('tabs', 'filament/schemas') }}"
wire:ignore.self
{{
$attributes
@@ -39,13 +70,7 @@
>
<input
type="hidden"
value="{{
collect($getChildSchema()->getComponents())
->filter(static fn (Tab $tab): bool => $tab->isVisible())
->map(static fn (Tab $tab) => $tab->getKey(isAbsolute: false))
->values()
->toJson()
}}"
value="{{ collect($tabs)->filter(static fn (Tab $tab): bool => $tab->isVisible())->map(static fn (Tab $tab) => $tab->getKey(isAbsolute: false))->values()->toJson() }}"
x-ref="tabsData"
/>
@@ -54,69 +79,126 @@
:label="$label"
:vertical="$isVertical"
x-cloak
:x-bind:class="! $isScrollable ? '{ \'fi-invisible\': ! withinDropdownMounted }' : null"
>
@foreach ($getStartRenderHooks() as $startRenderHook)
{{ \Filament\Support\Facades\FilamentView::renderHook($startRenderHook, scopes: $renderHookScopes) }}
@endforeach
@foreach ($getChildSchema()->getComponents() as $tab)
@foreach ($tabs as $index => $tab)
@php
$tabKey = $tab->getKey(isAbsolute: false);
$tabBadge = $tab->getBadge();
$tabBadgeColor = $tab->getBadgeColor();
$tabBadgeIcon = $tab->getBadgeIcon();
$tabBadgeIconPosition = $tab->getBadgeIconPosition();
$tabBadgeTooltip = $tab->getBadgeTooltip();
$tabExtraAttributeBag = $tab->getExtraAttributeBag();
$tabIcon = $tab->getIcon();
$tabIconPosition = $tab->getIconPosition();
$tabExtraAttributeBag = $tab->getExtraAttributeBag();
$tabHiddenJs = $tab->getHiddenJs();
$tabVisibleJs = $tab->getVisibleJs();
$tabVisibilityJs = match ([filled($tabHiddenJs), filled($tabVisibleJs)]) {
[true, true] => "(! ({$tabHiddenJs})) && ({$tabVisibleJs})",
[true, false] => "! ({$tabHiddenJs})",
[false, true] => $tabVisibleJs,
default => null,
};
$tabKey = $tab->getKey(isAbsolute: false);
$tabLabel = $tab->getLabel();
$tabVisibilityJs = $getTabVisibilityJs($tab, $index, 'inline');
@endphp
<x-filament::tabs.item
:alpine-active="'tab === \'' . $tabKey . '\''"
:attributes="$tabExtraAttributeBag"
:badge="$tabBadge"
:badge-color="$tabBadgeColor"
:badge-icon="$tabBadgeIcon"
:badge-icon-position="$tabBadgeIconPosition"
:badge-tooltip="$tabBadgeTooltip"
:data-tab-key="$tabKey"
:icon="$tabIcon"
:icon-position="$tabIconPosition"
:x-cloak="$tabVisibilityJs !== null"
:x-on:click="'tab = \'' . $tabKey . '\''"
:x-show="$tabVisibilityJs"
:x-cloak="$tabVisibilityJs !== null"
:attributes="$tabExtraAttributeBag"
>
{{ $tab->getLabel() }}
{{ $tabLabel }}
</x-filament::tabs.item>
@endforeach
@if (! $isScrollable)
<x-filament::dropdown
:placement="__('filament-panels::layout.direction') === 'ltr' ? 'bottom-start' : 'bottom-end'"
>
<x-slot name="trigger">
@foreach ($tabs as $index => $tab)
@php
$tabBadge = $tab->getBadge();
$tabBadgeColor = $tab->getBadgeColor();
$tabBadgeTooltip = $tab->getBadgeTooltip();
$tabExtraAttributeBag = $tab->getExtraAttributeBag();
$tabKey = $tab->getKey(isAbsolute: false);
$tabLabel = $tab->getLabel();
$tabVisibilityJs = $getTabVisibilityJs($tab, $index, 'trigger');
@endphp
<x-filament::tabs.item
:alpine-active="'tab === \'' . $tabKey . '\''"
:attributes="$tabExtraAttributeBag"
:badge="$tabBadge"
:badge-color="$tabBadgeColor"
:badge-tooltip="$tabBadgeTooltip"
:icon="Heroicon::ChevronDown"
:icon-alias="SchemaIconAlias::COMPONENTS_TABS_DROPDOWN_TRIGGER_BUTTON"
:x-cloak="$tabVisibilityJs !== null"
:x-show="$tabVisibilityJs"
>
{{ $tabLabel }}
</x-filament::tabs.item>
@endforeach
<x-filament::tabs.item x-show="isDropdownButtonVisible">
{{
\Filament\Support\generate_icon_html(
Heroicon::EllipsisHorizontal,
alias: SchemaIconAlias::COMPONENTS_TABS_MORE_TABS_BUTTON,
)
}}
</x-filament::tabs.item>
</x-slot>
<x-filament::dropdown.list>
@foreach ($tabs as $index => $tab)
@php
$tabBadge = $tab->getBadge();
$tabBadgeColor = $tab->getBadgeColor();
$tabBadgeTooltip = $tab->getBadgeTooltip();
$tabIcon = $tab->getIcon();
$tabKey = $tab->getKey(isAbsolute: false);
$tabLabel = $tab->getLabel();
@endphp
<x-filament::dropdown.list.item
:badge="$tabBadge"
:badge-color="$tabBadgeColor"
:badge-tooltip="$tabBadgeTooltip"
:icon="$tabIcon"
x-bind:class="{ 'fi-selected': tab === '{{ $tabKey }}' }"
:x-on:click="'tab = \'' . $tabKey . '\'; close($event);'"
:x-show="$index . ' >= withinDropdownIndex'"
>
{{ $tabLabel }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
</x-filament::dropdown>
@endif
@foreach ($getEndRenderHooks() as $endRenderHook)
{{ \Filament\Support\Facades\FilamentView::renderHook($endRenderHook, scopes: $renderHookScopes) }}
@endforeach
</x-filament::tabs>
@foreach ($getChildSchema()->getComponents() as $tab)
@foreach ($tabs as $tab)
@php
$tabHiddenJs = $tab->getHiddenJs();
$tabVisibleJs = $tab->getVisibleJs();
$tabVisibilityJs = match ([filled($tabHiddenJs), filled($tabVisibleJs)]) {
[true, true] => "(! ({$tabHiddenJs})) && ({$tabVisibleJs})",
[true, false] => "! ({$tabHiddenJs})",
[false, true] => $tabVisibleJs,
default => null,
};
$tabVisibilityJs = $getTabVisibilityJs($tab);
@endphp
@if ($tabVisibilityJs)
<div x-show="{!! $tabVisibilityJs !!}" x-cloak>
<div x-cloak x-show="{!! $tabVisibilityJs !!}">
{{ $tab }}
</div>
@else
@@ -160,14 +242,16 @@
$tabBadgeIcon = $tab->getBadgeIcon();
$tabBadgeIconPosition = $tab->getBadgeIconPosition();
$tabBadgeTooltip = $tab->getBadgeTooltip();
$tabExtraAttributeBag = $tab->getExtraAttributeBag();
$tabIcon = $tab->getIcon();
$tabIconPosition = $tab->getIconPosition();
$tabExtraAttributeBag = $tab->getExtraAttributeBag();
$tabKey = strval($tabKey);
$tabLabel = $tab->getLabel() ?? $this->generateTabLabel($tabKey);
@endphp
<x-filament::tabs.item
:active="$activeTab === $tabKey"
:attributes="$tabExtraAttributeBag"
:badge="$tabBadge"
:badge-color="$tabBadgeColor"
:badge-icon="$tabBadgeIcon"
@@ -176,9 +260,8 @@
:icon="$tabIcon"
:icon-position="$tabIconPosition"
:wire:click="'$set(\'' . $livewireProperty . '\', ' . (filled($tabKey) ? ('\'' . $tabKey . '\'') : 'null') . ')'"
:attributes="$tabExtraAttributeBag"
>
{{ $tab->getLabel() ?? $this->generateTabLabel($tabKey) }}
{{ $tabLabel }}
</x-filament::tabs.item>
@endforeach
+14
View File
@@ -39,6 +39,8 @@ class Tabs extends Component
protected string | Closure | null $livewireProperty = null;
protected bool | Closure $isScrollable = true;
protected bool | Closure $isVertical = false;
final public function __construct(string | Htmlable | Closure | null $label = null)
@@ -184,6 +186,18 @@ class Tabs extends Component
return $this->evaluate($this->livewireProperty);
}
public function scrollable(bool | Closure $condition = true): static
{
$this->isScrollable = $condition;
return $this;
}
public function isScrollable(): bool
{
return (bool) $this->evaluate($this->isScrollable);
}
public function vertical(bool | Closure $condition = true): static
{
$this->isVertical = $condition;
@@ -4,5 +4,9 @@ namespace Filament\Schemas\View;
class SchemaIconAlias
{
const COMPONENTS_WIZARD_COMPLETED_STEP = 'schema::components.wizard.completed-step';
public const COMPONENTS_TABS_DROPDOWN_TRIGGER_BUTTON = 'schema::components.tabs.dropdown-trigger-button';
public const COMPONENTS_TABS_MORE_TABS_BUTTON = 'schema::components.tabs.more-tabs-button';
public const COMPONENTS_WIZARD_COMPLETED_STEP = 'schema::components.wizard.completed-step';
}