Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions docs/src/routes/components/tabs/examples/disabled.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Tabs.Root class="tabs-root">
<Tabs.Root class="max-w-[400px] ml-4">
<Tabs.List>
<Tabs.Trigger class="tabs-trigger">Tab 1</Tabs.Trigger>
<Tabs.Trigger class="tabs-trigger" disabled>
<Tabs.Trigger class="p-2 border-b border-[#202a2c] mb-2 cursor-pointer hover:bg-[#202a2c] hover:outline-none focus-visible:outline-none focus-visible:bg-[#202a2c] ui-selected:border-b ui-selected:border-[#0088cb]">Tab 1</Tabs.Trigger>
<Tabs.Trigger class="p-2 border-b border-[#202a2c] mb-2 cursor-pointer hover:bg-[#202a2c] hover:outline-none focus-visible:outline-none focus-visible:bg-[#202a2c] ui-selected:border-b ui-selected:border-[#0088cb] disabled:text-[#4c6b6e] disabled:cursor-not-allowed" disabled={isDisabled.value}>
Tab 2
</Tabs.Trigger>
<Tabs.Trigger class="tabs-trigger">Tab 3</Tabs.Trigger>
<Tabs.Trigger class="p-2 border-b border-[#202a2c] mb-2 cursor-pointer hover:bg-[#202a2c] hover:outline-none focus-visible:outline-none focus-visible:bg-[#202a2c] ui-selected:border-b ui-selected:border-[#0088cb]">Tab 3</Tabs.Trigger>
</Tabs.List>
<Tabs.Content>Content 1</Tabs.Content>
<Tabs.Content>Content 2</Tabs.Content>
Expand Down
8 changes: 4 additions & 4 deletions docs/src/routes/components/tabs/examples/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ export default component$(() => {
useStyles$(tabsStyles);

return (
<Tabs.Root class="tabs-root">
<Tabs.Root class="max-w-[400px] ml-4">
<Tabs.List>
<Tabs.Trigger class="tabs-trigger">Tab 1</Tabs.Trigger>
<Tabs.Trigger class="tabs-trigger">Tab 2</Tabs.Trigger>
<Tabs.Trigger class="tabs-trigger">Tab 3</Tabs.Trigger>
<Tabs.Trigger class="p-2 border-b border-[#202a2c] mb-2 cursor-pointer hover:bg-[#202a2c] hover:outline-none focus-visible:outline-none focus-visible:bg-[#202a2c] ui-selected:border-b ui-selected:border-[#0088cb]">Tab 1</Tabs.Trigger>
<Tabs.Trigger class="p-2 border-b border-[#202a2c] mb-2 cursor-pointer hover:bg-[#202a2c] hover:outline-none focus-visible:outline-none focus-visible:bg-[#202a2c] ui-selected:border-b ui-selected:border-[#0088cb]">Tab 2</Tabs.Trigger>
<Tabs.Trigger class="p-2 border-b border-[#202a2c] mb-2 cursor-pointer hover:bg-[#202a2c] hover:outline-none focus-visible:outline-none focus-visible:bg-[#202a2c] ui-selected:border-b ui-selected:border-[#0088cb]">Tab 3</Tabs.Trigger>
</Tabs.List>
<Tabs.Content>Content 1</Tabs.Content>
<Tabs.Content>Content 2</Tabs.Content>
Expand Down
8 changes: 4 additions & 4 deletions docs/src/routes/components/tabs/examples/vertical.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ export default component$(() => {
useStyles$(tabsStyles);

return (
<Tabs.Root class="tabs-root" orientation="vertical">
<Tabs.Root class="max-w-[400px] ml-4" orientation="vertical">
<Tabs.List>
<Tabs.Trigger class="tabs-trigger">Tab 1</Tabs.Trigger>
<Tabs.Trigger class="tabs-trigger">Tab 2</Tabs.Trigger>
<Tabs.Trigger class="tabs-trigger">Tab 3</Tabs.Trigger>
<Tabs.Trigger class="p-2 border-b border-[#202a2c] mb-2 cursor-pointer hover:bg-[#202a2c] hover:outline-none focus-visible:outline-none focus-visible:bg-[#202a2c] ui-selected:border-b ui-selected:border-[#0088cb] ui-orientation-vertical:border-r ui-orientation-vertical:border-r-[#202a2c] ui-orientation-vertical:border-b-0 ui-selected:ui-orientation-vertical:border-r ui-selected:ui-orientation-vertical:border-r-[#0088cb] ui-orientation-vertical:mr-8 ui-orientation-vertical:mt-[-0.5rem]">Tab 1</Tabs.Trigger>
<Tabs.Trigger class="p-2 border-b border-[#202a2c] mb-2 cursor-pointer hover:bg-[#202a2c] hover:outline-none focus-visible:outline-none focus-visible:bg-[#202a2c] ui-selected:border-b ui-selected:border-[#0088cb] ui-orientation-vertical:border-r ui-orientation-vertical:border-r-[#202a2c] ui-orientation-vertical:border-b-0 ui-selected:ui-orientation-vertical:border-r ui-selected:ui-orientation-vertical:border-r-[#0088cb] ui-orientation-vertical:mr-8 ui-orientation-vertical:mt-[-0.5rem]">Tab 2</Tabs.Trigger>
<Tabs.Trigger class="p-2 border-b border-[#202a2c] mb-2 cursor-pointer hover:bg-[#202a2c] hover:outline-none focus-visible:outline-none focus-visible:bg-[#202a2c] ui-selected:border-b ui-selected:border-[#0088cb] ui-orientation-vertical:border-r ui-orientation-vertical:border-r-[#202a2c] ui-orientation-vertical:border-b-0 ui-selected:ui-orientation-vertical:border-r ui-selected:ui-orientation-vertical:border-r-[#0088cb] ui-orientation-vertical:mr-8 ui-orientation-vertical:mt-[-0.5rem]">Tab 3</Tabs.Trigger>
</Tabs.List>
<Tabs.Content>Content 1</Tabs.Content>
<Tabs.Content>Content 2</Tabs.Content>
Expand Down
17 changes: 17 additions & 0 deletions docs/src/routes/components/tabs/index.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Hero />

## Vertical

<Vertical />

## Disabled

<Disabled />
7 changes: 1 addition & 6 deletions libs/components/src/tabs/tabs-list.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Render
data-qds-tabs-list
role="tablist"
fallback="div"
data-orientation={
context.orientationSig.value === "vertical" ? "vertical" : "horizontal"
}
{...props}
>
<Slot />
Expand Down
13 changes: 10 additions & 3 deletions libs/components/src/tabs/tabs-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type TabsRootProps = Omit<PropsOf<"div">, "align" | "onChange$"> &
value: string;
orientation: "horizontal" | "vertical";
loop: boolean;
disabled: boolean;
}> & {
onChange$?: (value: string) => void;
selectOnFocus?: boolean;
Expand All @@ -35,6 +36,7 @@ type TabsContext = {
selectOnFocus: boolean;
currTriggerIndex: number;
currContentIndex: number;
isDisabled: Signal<boolean>;
};

export const TabsRoot = component$((props: TabsRootProps) => {
Expand All @@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like something we would just have a boolean for, not needing a bind

disabled: false,
});

const context: TabsContext = {
Expand All @@ -67,7 +71,8 @@ export const TabsRoot = component$((props: TabsRootProps) => {
loopSig,
selectOnFocus,
currTriggerIndex,
currContentIndex
currContentIndex,
isDisabled,
};

useContextProvider(tabsContextId, context);
Expand All @@ -87,10 +92,12 @@ export const TabsRoot = component$((props: TabsRootProps) => {
return (
<Render
data-qds-tabs-root
data-qds-scope
fallback="div"
data-orientation={
context.orientationSig.value === "vertical" ? "vertical" : "horizontal"
}
data-disabled={isDisabled.value}
{...rest}
>
<Slot />
Expand Down
4 changes: 1 addition & 3 deletions libs/components/src/tabs/tabs-trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
>
<Slot />
Expand Down
34 changes: 33 additions & 1 deletion libs/components/styles/tailwind/data-attrs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type DataAttributeState =
| "empty"
| "orientation";

type OrientationValue = "vertical" | "horizontal";

/**
* Generate positive and negative variant CSS for a given state
*
Expand Down Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to re-think this approach in a more general sense, like generateValueVariant

Also:

/**
 * 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"]&
);

This would quickly get messy needing to create a custom variant for every possible value. Maybe they can provide a "arbitrary variant" the same way they provide a value?

If we don't want to come at it from that angle, I think it would make sense to add two new modifiers:

ui-horizontal and ui-vertical.

Also making sure that we add the not-x versions as well so people can compose them as they would expect.

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
*/
Expand All @@ -80,6 +105,8 @@ export function generateAllVariants(): string {
"orientation"
];

const orientations: OrientationValue[] = ["vertical", "horizontal"];

const header = `/**
* QDS UI State Variants
*
Expand All @@ -90,13 +117,18 @@ 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
*/
`;

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}`;
}
26 changes: 26 additions & 0 deletions libs/components/styles/tailwind/qds-tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]&
);
Loading