Skip to content

Commit

Permalink
feat: tabs-next (#2541)
Browse files Browse the repository at this point in the history
Co-authored-by: Mahmoud-zino <mahmoud.alhalaby@gmail.cim>
Co-authored-by: endigo9740 <gundamx9740@gmail.com>
  • Loading branch information
3 people authored Mar 29, 2024
1 parent 007f848 commit 201d370
Show file tree
Hide file tree
Showing 26 changed files with 667 additions and 153 deletions.
9 changes: 7 additions & 2 deletions packages/skeleton-react/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// NOTE: do not delete the above comment. It's required for local HMR on plugin changes.

import { skeleton } from "@skeletonlabs/skeleton/plugin";
import { cerberus } from "@skeletonlabs/skeleton/themes";
import * as themes from "@skeletonlabs/skeleton/themes";

/** @type {import('tailwindcss').Config} */
export default {
Expand All @@ -14,7 +14,12 @@ export default {
plugins: [
require('@tailwindcss/forms'),
skeleton({
themes: [cerberus],
themes: [
themes.cerberus,
themes.catppuccin,
themes.pine,
themes.rose,
]
}),
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
lead,
trail,
headline
} = $props<AppBarProps>();
}: AppBarProps = $props();
</script>

<!-- @component A header element for the top of a page layout. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
imageClasses = '',
// Snippets
children
} = $props<AvatarProps>();
}: AvatarProps = $props();
</script>

<!-- @component An image with a fallback for representing the user. -->
Expand Down
33 changes: 33 additions & 0 deletions packages/skeleton-svelte/src/lib/components/Tab/Tabs.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
import type { TabsProps } from './types.js';
let {
id,
// Root
base = 'w-full',
spaceY = 'space-y-4',
classes = '',
// Tab List
listBase = 'flex',
listJustify = 'justify-start',
listGap = 'gap-2',
listBorder = 'border-b-[1px] border-surface-200-800',
listClasses = '',
// Snippets
list,
panels
}: TabsProps = $props();
</script>

<!-- @component A Tab parent component. -->

<div {id} class="{base} {spaceY} {classes}" data-testid="tabs">
{#if list}
<div class="{listBase} {listGap} {listJustify} {listBorder} {listClasses}" role="tablist">
{@render list()}
</div>
{/if}
{#if panels}
{@render panels()}
{/if}
</div>
138 changes: 138 additions & 0 deletions packages/skeleton-svelte/src/lib/components/Tab/TabsControl.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<script lang="ts">
import type { TabsControlProps } from './types.js';
let {
id,
name,
group,
title,
// A11y
label = '',
controls = '',
// Root
base = 'group',
active = 'text-surface-950-50 border-surface-950-50',
inactive = 'text-surface-600-400 border-transparent',
flex = 'flex justify-center items-center',
background = '',
border = 'border-b-[1px]',
text = 'type-scale-3',
padding = 'pb-2',
rounded = '',
gap = 'gap-1',
cursor = 'cursor-pointer',
classes = '',
// Content
contentBase = 'w-full',
contentFlex = 'flex justify-center items-center',
contentGap = 'gap-2',
contentBg = 'group-hover:preset-tonal-primary',
contentPadding = 'p-2 px-4',
contentRounded = 'rounded',
contentClasses = '',
// Events
onclick = () => {},
onkeypress = () => {},
onkeydown = () => {},
onkeyup = () => {},
onchange = () => {},
// Snippets
children
}: TabsControlProps = $props();
const selected = $derived(group === name);
const rxActive = $derived(selected ? active : inactive);
let elemInput: HTMLInputElement;
function onKeyDownHandler(event: KeyboardEvent) {
// Fire Event Handler
onkeydown(event);
// If select key events
if (!['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(event.code)) return;
// Prevent default behavior
event.preventDefault();
// Find the closest tab/tablelist
const currTab = elemInput.closest('[role="tab"]');
if (!currTab) return;
const tabList = elemInput.closest('[role="tablist"]');
if (!tabList) return;
// Get RTL mode
const isRTL = getComputedStyle(tabList).direction === 'rtl';
// Get list of tab elements
const tabs = Array.from(tabList.querySelectorAll('[role="tab"]'));
// Get a reference to the current tab
const currIndex = tabs.indexOf(currTab);
// Determine the index of the next tab
let nextIndex = -1;
switch (event.code) {
case 'ArrowRight':
if (isRTL) {
nextIndex = currIndex - 1 < 0 ? tabs.length - 1 : currIndex - 1;
break;
}
nextIndex = currIndex + 1 >= tabs.length ? 0 : currIndex + 1;
break;
case 'ArrowLeft':
if (isRTL) {
nextIndex = currIndex + 1 >= tabs.length ? 0 : currIndex + 1;
break;
}
nextIndex = currIndex - 1 < 0 ? tabs.length - 1 : currIndex - 1;
break;
case 'Home':
nextIndex = 0;
break;
case 'End':
nextIndex = tabs.length - 1;
break;
}
if (nextIndex < 0) return;
// Set Active Tab
const nextTab = tabs![nextIndex!];
const nextTabInput = nextTab?.querySelector('input');
if (nextTabInput) {
nextTabInput.click();
(nextTab as HTMLDivElement).focus();
}
}
</script>

<!-- @component A Tab Control component. -->

<label
{id}
class="{base} {rxActive} {flex} {background} {border} {text} {padding} {rounded} {gap} {cursor} {classes}"
aria-label={label}
{title}
>
<!-- NOTE: do not add additional classes to this <div> -->
<div
class="size-full"
role="tab"
aria-controls={controls}
aria-selected={selected}
data-testid="tabs-control"
tabindex={selected ? 0 : -1}
onkeydown={onKeyDownHandler}
{onkeypress}
{onkeyup}
>
<!-- Keep these classes on wrapping element -->
<div class="h-0 w-0 flex-none overflow-hidden">
<input bind:group bind:this={elemInput} type="radio" {name} value={name} onchange={() => onchange(group)} {onclick} tabindex="-1" />
</div>
<!-- Content -->
{#if children}
<div class="{contentBase} {contentFlex} {contentGap} {contentBg} {contentPadding} {contentRounded} {contentClasses}">
{@render children()}
</div>
{/if}
</div>
</label>
23 changes: 23 additions & 0 deletions packages/skeleton-svelte/src/lib/components/Tab/TabsPanel.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import type { TabsPanelProps } from './types.js';
let {
id,
value,
group,
// A11y
labelledBy,
// Root
classes = '',
// Snippets
children
}: TabsPanelProps = $props();
</script>

<!-- @component A Tab Panel component. -->

{#if value === group && children}
<div {id} role="tabpanel" tabindex="0" aria-labelledby={labelledBy} class={classes}>
{@render children()}
</div>
{/if}
5 changes: 5 additions & 0 deletions packages/skeleton-svelte/src/lib/components/Tab/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Tabs from './Tabs.svelte';
import Control from './TabsControl.svelte';
import Panel from './TabsPanel.svelte';

export default Object.assign(Tabs, { Control, Panel });
138 changes: 138 additions & 0 deletions packages/skeleton-svelte/src/lib/components/Tab/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { Snippet } from 'svelte';

// Tabs ---

export interface TabsProps {
/** Provide a unique ID. */
id?: string;

// Root ---
/** Sets base styles. */
base?: string;
/** Set vertical spacing between list and panels. */
spaceY?: string;
/** Provide arbitrary CSS classes. */
classes?: string;

// Tab list ---
/** Sets the list snippet element's base styles. */
listBase?: string;
/** Sets the list snippet element's justification styles. */
listJustify?: string;
/** Sets the list snippet element's gap spacing. */
listGap?: string;
/** Sets the list snippet element's border styles. */
listBorder?: string;
/** Provide arbitrary CSS classes to the list snippet. */
listClasses?: string;

// Tab panel ---
/** Provide arbitrary CSS classes to the tab panel snippet. */
panelClasses?: string;

// Snippets ---
/** The tab list slot. */
list?: Snippet;
/** The tab panel slot. */
panels?: Snippet;
}

// TabControl ---

export interface TabsControlProps {
/** Provide a unique ID. */
id?: string;
/** Provide the tab control name. */
name: string;
/** Provide the tab control radio group. */
group: string;
/** Provide a hoverable title attribute. */
title?: string;

// A11y ---
/** Sets the A11y label. */
label?: string;
/** Sets ARIA controls value to define which panel this tab controls. */
controls?: string;

// Root ---
/** Sets base styles. */
base?: string;
/** Sets the active control styles. */
active?: string;
/** Sets the inactive control styles. */
inactive?: string;
/** Sets flex styles. */
flex?: string;
/** Sets background styles. */
background?: string;
/** Sets border styles. */
border?: string;
/** Sets text size styles. */
text?: string;
/** Sets padding styles. */
padding?: string;
/** Sets rounded styles. */
rounded?: string;
/** Sets vertical gap styles. */
gap?: string;
/** Sets cursor styles. */
cursor?: string;
/** Provide arbitrary CSS classes. */
classes?: string;

// Tab ---
/** Sets tab content base styles. */
contentBase?: string;
/** Sets tab content flex styles. */
contentFlex?: string;
/** Sets the tab content gap styles. */
contentGap?: string;
/** Sets the tab content background styles. */
contentBg?: string;
/** Sets the tab content padding styles. */
contentPadding?: string;
/** Sets the tab content rounded styles. */
contentRounded?: string;
/** Provide arbitrary CSS classes for the tab content. */
contentClasses?: string;

// Events ---
/** Triggers on Tab Control click. */
onclick?: (event: MouseEvent) => void;
/** Triggers on Tab Control key press. */
onkeypress?: (event: KeyboardEvent) => void;
/** Triggers on Tab Control key down. */
onkeydown?: (event: KeyboardEvent) => void;
/** Triggers on Tab Control key up. */
onkeyup?: (event: KeyboardEvent) => void;
/** Triggers on Tab Control group change. */
onchange?: (group: string) => void;

// Snippets ---
/** The default child slot. */
children?: Snippet;
}

// TabPanel ---

export interface TabsPanelProps {
/** Provide a unique ID. */
id?: string;
/** Provide the tab panel value. */
value: string;
/** Provide the tab control radio group. */
group: string;

// A11y ---
/** Sets the A11y labelledby. */
labelledBy?: string;

// Root ---
/** Provide arbitrary CSS classes. */
classes?: string;

// Snippets ---
/** The default child slot. */
children?: Snippet;
}
1 change: 1 addition & 0 deletions packages/skeleton-svelte/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { default as Accordion } from './components/Accordion/Accordion.svelte';
export { default as AccordionItem } from './components/Accordion/AccordionItem.svelte';
export { default as Avatar } from './components/Avatar/Avatar.svelte';
export { default as AppBar } from './components/AppBar/AppBar.svelte';
export { default as Tabs } from './components/Tab/index.js';
1 change: 1 addition & 0 deletions packages/skeleton-svelte/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<a class="anchor" href="/components/accordions">Accordions</a>
<a class="anchor" href="/components/avatars">Avatars</a>
<a class="anchor" href="/components/app-bars">App Bars</a>
<a class="anchor" href="/components/tabs">Tabs</a>
<a class="anchor" href="/components/progress">Progress</a>
</nav>
</div>
Expand Down
Loading

0 comments on commit 201d370

Please sign in to comment.