From 090059c9e7a503b2ad2cfd3bfd2fb9c698769879 Mon Sep 17 00:00:00 2001 From: toan-kunaico <122486240+toan-kunaico@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:29:51 -0700 Subject: [PATCH] feat: tabs data attrs --- .../components/tabs/examples/disabled.tsx | 12 ++++--- .../routes/components/tabs/examples/hero.tsx | 8 ++--- .../components/tabs/examples/vertical.tsx | 8 ++--- docs/src/routes/components/tabs/index.mdx | 17 ++++++++++ libs/components/src/tabs/tabs-list.tsx | 7 +--- libs/components/src/tabs/tabs-root.tsx | 13 +++++-- libs/components/src/tabs/tabs-trigger.tsx | 4 +-- libs/components/styles/tailwind/data-attrs.ts | 34 ++++++++++++++++++- .../styles/tailwind/qds-tailwind.css | 26 ++++++++++++++ 9 files changed, 103 insertions(+), 26 deletions(-) diff --git a/docs/src/routes/components/tabs/examples/disabled.tsx b/docs/src/routes/components/tabs/examples/disabled.tsx index e01dd573e..6698810e7 100644 --- a/docs/src/routes/components/tabs/examples/disabled.tsx +++ b/docs/src/routes/components/tabs/examples/disabled.tsx @@ -1,18 +1,20 @@ import { Tabs } from "@qds.dev/ui"; -import { component$, useStyles$ } from "@qwik.dev/core"; +import { component$, useSignal, useStyles$ } from "@qwik.dev/core"; import tabsStyles from "./tabs.css?inline"; export default component$(() => { useStyles$(tabsStyles); + const isDisabled = useSignal(true); + return ( - + - Tab 1 - + Tab 1 + Tab 2 - Tab 3 + Tab 3 Content 1 Content 2 diff --git a/docs/src/routes/components/tabs/examples/hero.tsx b/docs/src/routes/components/tabs/examples/hero.tsx index a08315250..d887db597 100644 --- a/docs/src/routes/components/tabs/examples/hero.tsx +++ b/docs/src/routes/components/tabs/examples/hero.tsx @@ -6,11 +6,11 @@ export default component$(() => { useStyles$(tabsStyles); return ( - + - Tab 1 - Tab 2 - Tab 3 + Tab 1 + Tab 2 + Tab 3 Content 1 Content 2 diff --git a/docs/src/routes/components/tabs/examples/vertical.tsx b/docs/src/routes/components/tabs/examples/vertical.tsx index 71b1ba07b..329dd41ac 100644 --- a/docs/src/routes/components/tabs/examples/vertical.tsx +++ b/docs/src/routes/components/tabs/examples/vertical.tsx @@ -6,11 +6,11 @@ export default component$(() => { useStyles$(tabsStyles); return ( - + - Tab 1 - Tab 2 - Tab 3 + Tab 1 + Tab 2 + Tab 3 Content 1 Content 2 diff --git a/docs/src/routes/components/tabs/index.mdx b/docs/src/routes/components/tabs/index.mdx index e69de29bb..81a7f501a 100644 --- a/docs/src/routes/components/tabs/index.mdx +++ b/docs/src/routes/components/tabs/index.mdx @@ -0,0 +1,17 @@ +import Hero from "./examples/hero"; +import Vertical from "./examples/vertical"; +import Disabled from "./examples/disabled"; + +# Tabs + +A set of layered sections of content, known as tab panels, that are displayed one at a time. + + + +## Vertical + + + +## Disabled + + \ No newline at end of file diff --git a/libs/components/src/tabs/tabs-list.tsx b/libs/components/src/tabs/tabs-list.tsx index 9110bc118..de9f31438 100644 --- a/libs/components/src/tabs/tabs-list.tsx +++ b/libs/components/src/tabs/tabs-list.tsx @@ -1,20 +1,15 @@ -import { type PropsOf, Slot, component$, useContext } from "@qwik.dev/core"; +import { type PropsOf, Slot, component$ } from "@qwik.dev/core"; import { Render } from "../render/render"; -import { tabsContextId } from "./tabs-root"; export type TabsListProps = PropsOf<"div">; export const TabsList = component$((props: TabsListProps) => { - const context = useContext(tabsContextId); return ( diff --git a/libs/components/src/tabs/tabs-root.tsx b/libs/components/src/tabs/tabs-root.tsx index 65192a124..14717012f 100644 --- a/libs/components/src/tabs/tabs-root.tsx +++ b/libs/components/src/tabs/tabs-root.tsx @@ -18,6 +18,7 @@ export type TabsRootProps = Omit, "align" | "onChange$"> & value: string; orientation: "horizontal" | "vertical"; loop: boolean; + disabled: boolean; }> & { onChange$?: (value: string) => void; selectOnFocus?: boolean; @@ -35,6 +36,7 @@ type TabsContext = { selectOnFocus: boolean; currTriggerIndex: number; currContentIndex: number; + isDisabled: Signal; }; export const TabsRoot = component$((props: TabsRootProps) => { @@ -52,12 +54,14 @@ export const TabsRoot = component$((props: TabsRootProps) => { const { valueSig: selectedValueSig, orientationSig, - loopSig + loopSig, + disabledSig: isDisabled, } = useBindings(props, { value: "0", orientation: "horizontal", loop: false, - selectOnFocus: true + selectOnFocus: true, + disabled: false, }); const context: TabsContext = { @@ -67,7 +71,8 @@ export const TabsRoot = component$((props: TabsRootProps) => { loopSig, selectOnFocus, currTriggerIndex, - currContentIndex + currContentIndex, + isDisabled, }; useContextProvider(tabsContextId, context); @@ -87,10 +92,12 @@ export const TabsRoot = component$((props: TabsRootProps) => { return ( diff --git a/libs/components/src/tabs/tabs-trigger.tsx b/libs/components/src/tabs/tabs-trigger.tsx index f8f14bdb4..255f16c80 100644 --- a/libs/components/src/tabs/tabs-trigger.tsx +++ b/libs/components/src/tabs/tabs-trigger.tsx @@ -151,15 +151,13 @@ export const TabsTrigger = component$((props: TabsTriggerProps) => { data-qds-tabs-trigger role="tab" fallback="button" - data-orientation={ - context.orientationSig.value === "vertical" ? "vertical" : "horizontal" - } onClick$={[handleSelect$, props.onClick$]} onFocus$={[context.selectOnFocus ? handleSelect$ : undefined, props.onFocus$]} onKeyDown$={[handleKeyDownSync$, handleNavigation$, props.onKeyDown$]} tabIndex={isSelectedSig.value ? 0 : -1} data-selected={isSelectedSig.value} aria-selected={isSelectedSig.value ? "true" : "false"} + aria-disabled={context.isDisabled.value ? "true" : "false"} {...props} > diff --git a/libs/components/styles/tailwind/data-attrs.ts b/libs/components/styles/tailwind/data-attrs.ts index 25569ab34..ea097713a 100644 --- a/libs/components/styles/tailwind/data-attrs.ts +++ b/libs/components/styles/tailwind/data-attrs.ts @@ -24,6 +24,8 @@ type DataAttributeState = | "empty" | "orientation"; +type OrientationValue = "vertical" | "horizontal"; + /** * Generate positive and negative variant CSS for a given state * @@ -60,6 +62,29 @@ function generateVariant(state: DataAttributeState): string { );`; } +/** + * Generate orientation-specific variant CSS + * + * @param orientation - The orientation value ("vertical" or "horizontal") + * @returns CSS string with @custom-variant declarations + */ +function generateOrientationVariant(orientation: OrientationValue): string { + const variantName = `ui-orientation-${orientation}`; + const dataAttr = `data-orientation="${orientation}"`; + + return `/** + * ${variantName}: Apply styles when nearest component scope has ${dataAttr} + * Automatically scopes to prevent nested components from inheriting state + */ +@custom-variant ${variantName} ( + /* Descendant of scope with ${dataAttr}, stops at nearest scope boundary */ + [data-qds-scope][${dataAttr}] > &, + [data-qds-scope][${dataAttr}] > :not([data-qds-scope]) &, + /* Direct match on element itself */ + [${dataAttr}]& +);`; +} + /** * Generate all variants */ @@ -80,6 +105,8 @@ export function generateAllVariants(): string { "orientation" ]; + const orientations: OrientationValue[] = ["vertical", "horizontal"]; + const header = `/** * QDS UI State Variants * @@ -90,6 +117,8 @@ export function generateAllVariants(): string { * ui-checked:bg-blue-500 * not-ui-disabled:opacity-100 * ui-open:rotate-180 + * ui-orientation-vertical:border-r + * ui-orientation-horizontal:border-b * * Generated by: docs/generate-ui-variants.ts * DO NOT EDIT MANUALLY - Run the generator script to update @@ -97,6 +126,9 @@ export function generateAllVariants(): string { `; const variants = states.map((state) => generateVariant(state)).join("\n\n"); + const orientationVariants = orientations + .map((orientation) => generateOrientationVariant(orientation)) + .join("\n\n"); - return `${header}\n${variants}`; + return `${header}\n${variants}\n\n${orientationVariants}`; } diff --git a/libs/components/styles/tailwind/qds-tailwind.css b/libs/components/styles/tailwind/qds-tailwind.css index b6f269990..6e1c37434 100644 --- a/libs/components/styles/tailwind/qds-tailwind.css +++ b/libs/components/styles/tailwind/qds-tailwind.css @@ -20,6 +20,8 @@ * ui-checked:bg-blue-500 * not-ui-disabled:opacity-100 * ui-open:rotate-180 + * ui-orientation-vertical:border-r + * ui-orientation-horizontal:border-b * * Generated by: docs/generate-ui-variants.ts * DO NOT EDIT MANUALLY - Run the generator script to update @@ -335,4 +337,28 @@ [data-qds-scope]:not([data-orientation]) > :not([data-qds-scope]) &, /* Direct match on element itself ONLY if it's also a scope */ [data-qds-scope]:not([data-orientation])& +); + +/** + * ui-orientation-vertical: Apply styles when nearest component scope has data-orientation="vertical" + * Automatically scopes to prevent nested components from inheriting state + */ +@custom-variant ui-orientation-vertical ( + /* Descendant of scope with data-orientation="vertical", stops at nearest scope boundary */ + [data-qds-scope][data-orientation="vertical"] > &, + [data-qds-scope][data-orientation="vertical"] > :not([data-qds-scope]) &, + /* Direct match on element itself */ + [data-orientation="vertical"]& +); + +/** + * ui-orientation-horizontal: Apply styles when nearest component scope has data-orientation="horizontal" + * Automatically scopes to prevent nested components from inheriting state + */ +@custom-variant ui-orientation-horizontal ( + /* Descendant of scope with data-orientation="horizontal", stops at nearest scope boundary */ + [data-qds-scope][data-orientation="horizontal"] > &, + [data-qds-scope][data-orientation="horizontal"] > :not([data-qds-scope]) &, + /* Direct match on element itself */ + [data-orientation="horizontal"]& ); \ No newline at end of file