Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a new switch component #960

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
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
6 changes: 6 additions & 0 deletions .changeset/poor-spiders-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@qwik-ui/headless': major
'@qwik-ui/styled': major
---

add a new switch component
4 changes: 4 additions & 0 deletions apps/component-tests/src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@
--alert: 0 84.2% 60.2%;
--alert-foreground: 210 40% 98%;
--ring: 222.2 47.4% 11.2%;
--switch-thumb-color: 0 0% 100%;
--thumb-color-highlight: 0, 0%, 72%, 0.25;
}

.dark {
--thumb-color-highlight: 0, 0%, 100%, 0.25;
--switch-thumb-color: 0 0% 100%;
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { component$ } from '@builder.io/qwik';
import { ShowcaseTest } from '../../../../components/showcase-test/showcase-test';

export default component$(() => {
// Need to center the content in the screen
// so that tests like popover placement can
Expand Down
18 changes: 18 additions & 0 deletions apps/website/src/routes/docs/headless/switch/examples/checked.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { component$, useStyles$, useSignal } from '@builder.io/qwik';
import { Switch } from '@qwik-ui/headless';


export default component$(() => {
const checked = useSignal(true)
useStyles$(styles);
return (
<Switch.Root class="switch" bind:checked={checked}>
<Switch.Label>test</Switch.Label>
<Switch.Input />
</Switch.Root>
);
});

import styles from '../snippets/switch.css?inline';


Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { component$, useSignal } from '@builder.io/qwik';
import { Switch } from '@qwik-ui/headless';


export default component$(() => {
const checked = useSignal(false)
return (
<Switch.Root class="switch" defaultChecked bind:checked={checked}>
<Switch.Label>test</Switch.Label>
<Switch.Input />
</Switch.Root>
);
});



18 changes: 18 additions & 0 deletions apps/website/src/routes/docs/headless/switch/examples/disabled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Switch } from '@qwik-ui/headless';


export default component$(() => {
const checked = useSignal(false)
useStyles$(styles);
return (
<Switch.Root class="switch" disabled bind:checked={checked}>
<Switch.Label>test</Switch.Label>
<Switch.Input/>
</Switch.Root>
);
});

import styles from '../snippets/switch.css?inline';


24 changes: 24 additions & 0 deletions apps/website/src/routes/docs/headless/switch/examples/hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { component$, useSignal, useStyles$ } from '@builder.io/qwik';
import { Switch } from '@qwik-ui/headless';


export default component$(() => {
const checked = useSignal(false)
const count = useSignal(0);
useStyles$(styles);

return (
<Switch.Root
class="switch"
bind:checked={checked}
onChange$={() => count.value++}
>
<Switch.Label>test{count.value}</Switch.Label>
<Switch.Input />
</Switch.Root>
);
});

import styles from '../snippets/switch.css?inline';


274 changes: 274 additions & 0 deletions apps/website/src/routes/docs/headless/switch/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
---
title: Qwik UI | Tabs
---

import { statusByComponent } from '~/_state/component-statuses';

<StatusBanner status={statusByComponent.headless.Tabs} />

# Tabs

Explore and switch between different views using tabs

<Showcase name="first" />

## Building blocks

### The full version

<CodeSnippet name="long" />

### The short version

<CodeSnippet name="short" />

> The "short version" 👆 will be automatically transformed into the full ARIA compliant version

## Vertical

by default, the tabs are horizontal, but you can adjust the underlying behavior to be vertical by setting the `vertical` prop to true.

<Showcase name="vertical" />

## Disabled

You can disable a tab by setting the `disabled` prop to true.

<Showcase name="disabled" />

## Behavior

### Automatic

The default behavior of the tabs is manual, which means that the tabs will automatically switch to the next tab when the user hovers over the tab. You can change this behavior by setting the `behavior` prop to `automatic`.

<Showcase name="automatic" />

### Manual

You can set the behavior to manual, which means that the tabs will not automatically switch to the next tab when the user presses the right arrow key, and to the previous tab when the user presses the left arrow key.

<Showcase name="manual" />

## Dynamic

<Showcase name="dynamic" />

## onSelectedIndexChange$

You can listen to changes in the selected index by subscribing to the `onSelectedIndexChange$` store.

<Showcase name="on-selected-index-change" />

## bind:selectedIndex

You can provide a signal for the selected index with the `bind:selectedIndex={yourSignal}` and it will be used directly. This is a more efficient version of `onSelectedIndexChange$`.

## bind:selectedTabId

You can provide a signal for the selected tab id with the `bind:selectedTabId={yourSignal}`
and it will be used directly.

💡 You can manually set the `tabId` prop on each tab to be able to select any tab by its ID.

<Showcase name="selected-tab-id" />

## Add a "selected" prop

You can add a "selected" prop to the Tab component to make it selected by default.

<Showcase name="selected-prop" />

## onClick$

You can pass a custom `onClick$` handler.

<Showcase name="on-click" />

## Creating reusable Tabs

In order to remove the need for a linking `value` prop for each Tab and Tabs.Panel pair, we have chosen to create the Tabs component as an [inline component](https://qwik.builder.io/docs/components/overview/#inline-components).

By consequence, the Tabs component needs to be aware of its children. If you want to create your custom reusable Tabs.List/Tab/Tabs.Panel components, you will have to pass them to the Tabs component through its `Tabs.List`, `Tab`, and `Tabs.Panel` props:

```tsx
const CustomTabs = (props: PropsOf<typeof Tabs.Root>) => (
<Tabs.Root
{...props}
tabListComponent={CustomTabsList}
tabComponent={CustomTab}
tabPanelComponent={CustomTabsPanel}
/>
);
```

<br />

<Note status="warning">
If you don't do the above, the Tabs component cannot know if your CustomTab component is
a Tab component.
</Note>

<Showcase name="reusable" />

## Accessibility

### Keyboard interaction

<KeyboardInteractionTable
keyDescriptors={[
{
keyTitle: 'Tab',
description: 'Moves focus to the selected panel.',
},
{
keyTitle: 'Shift + Tab',
description: 'Moves focus to the selected tab.',
},
{
keyTitle: 'ArrowRight',
description: 'Moves focus to the next tab.',
},
{
keyTitle: 'ArrowLeft',
description: 'Moves focus to the previous tab.',
},
{
keyTitle: 'ArrowDown',
description: 'In "vertical mode", moves focus to the next tab.',
},
{
keyTitle: 'ArrowUp',
description: 'In "vertical mode", moves focus to the previous tab.',
},
{
keyTitle: 'Home',
description: 'Moves focus to the first tab.',
},
{
keyTitle: 'PageUp',
description: 'Moves focus to the first tab.',
},
{
keyTitle: 'End',
description: 'Moves focus to the last tab.',
},
{
keyTitle: 'PageDown',
description: 'Moves focus to the first tab.',
},
]}
/>

## API

### Tabs

<APITable
propDescriptors={[
{
name: 'behavior',
type: 'string',
description:
'Toggle between automatic or manual. The automatic behavior moves between tabs when hover. The manual behavior moves between tabs on click.',
},
{
name: 'selectedIndex',
type: 'number',
description: 'A way to set the selected index programmatically',
},
{
name: 'selectedTabId',
type: 'number',
description:
'A way to set the selected tabId programmatically. The tabId is set by the `key` prop of the Tab or Tabs.Panel',
},
{
name: 'vertical',
type: 'boolean',
description:
'If set to true, the behavior of UpArrow and DownArrow will navigate between tabs vertically instead of horizontally.',
},
{
name: 'onSelectedIndexChange$',
type: 'function',
info: '(index: number) => void',
description: 'An event hook that gets notified whenever the selected index changes',
},
{
name: 'onSelectedTabIdChange$',
type: 'function',
info: '(index: string) => void',
description: 'An event hook that gets notified whenever the selected tabId changes',
},
{
name: 'bind:selectedIndex',
type: 'signal',
info: 'Signal<number | undefined>',
description:
'A signal that binds the selected index. This is a more efficient version of `selectedIndex` + `onSelectedIndexChange$`',
},
{
name: 'bind:selectedTabId',
type: 'signal',
info: 'Signal<string | undefined>',
description:
'A signal that binds the selected tabId. This is a more efficient version of `selectedTabId` + `onSelectedTabIdChange$`',
},
{
name: 'tabClass',
type: 'string',
description: 'The class name to apply to tabs',
},
{
name: 'panelClass',
type: 'string',
description: 'The class name to apply to panels',
},
]}
/>

### Tab

<APITable
propDescriptors={[
{
name: 'selectedClassName',
type: 'string',
description: 'The class name to apply when the tab is selected',
},
{
name: 'selected',
type: 'boolean',
description: 'Select this tab by default',
},
{
name: 'disabled',
type: 'boolean',
description: 'Set the disabled state of a specific tab',
},
{
name: 'tabId',
type: 'string',
description:
'Set the tab id, can be used with `bind:selectedTabId` to select a tab programmatically',
},
]}
/>

### Tabs.Panel

<APITable
propDescriptors={[
{
name: 'label',
type: 'element',
description: 'Shorthand for `<Tab>{label}</Tab>`',
},
{
name: 'selected',
type: 'boolean',
description: 'Select this tab by default',
},
]}
/>
Loading
Loading