mirror of
https://github.com/penpot/penpot.git
synced 2026-09-25 13:26:16 +00:00
🎉 Apply fixes and new doc structure
This commit is contained in:
parent
913a8d3148
commit
1d6389a3eb
@ -28,7 +28,7 @@
|
|||||||
(mf/defc switcher*
|
(mf/defc switcher*
|
||||||
{::mf/forward-ref true
|
{::mf/forward-ref true
|
||||||
::mf/schema schema:switcher}
|
::mf/schema schema:switcher}
|
||||||
[{:keys [id label checked default-checked on-change disabled size aria-label class data-testid] :rest props} ref]
|
[{:keys [id label checked default-checked on-change disabled size aria-label class] :rest props} ref]
|
||||||
(let [id (or id (mf/use-id))
|
(let [id (or id (mf/use-id))
|
||||||
size (keyword (d/nilv size "md"))
|
size (keyword (d/nilv size "md"))
|
||||||
disabled (d/nilv disabled false)
|
disabled (d/nilv disabled false)
|
||||||
@ -68,12 +68,16 @@
|
|||||||
(handle-toggle event)))
|
(handle-toggle event)))
|
||||||
|
|
||||||
has-label (not (str/blank? label))
|
has-label (not (str/blank? label))
|
||||||
effective-aria-label (or aria-label (when-not has-label "Toggle switch"))]
|
effective-aria-label (if has-label
|
||||||
|
(or aria-label label)
|
||||||
|
"Toggle switch")]
|
||||||
|
|
||||||
[:div {:class (dm/str class " " (stl/css-case :switcher-wrapper true))}
|
[:div {:class (dm/str class " " (stl/css-case :switcher-wrapper true))
|
||||||
|
:data-testid (.-data-testid props)}
|
||||||
(when has-label
|
(when has-label
|
||||||
[:label {:for id
|
[:label {:for id
|
||||||
:class (stl/css :switcher-label)
|
:class (stl/css-case :switcher-label true
|
||||||
|
:is-disabled disabled)
|
||||||
:on-click handle-label-click}
|
:on-click handle-label-click}
|
||||||
label])
|
label])
|
||||||
[:div {:id id
|
[:div {:id id
|
||||||
@ -83,7 +87,6 @@
|
|||||||
:aria-checked current-checked
|
:aria-checked current-checked
|
||||||
:aria-disabled disabled
|
:aria-disabled disabled
|
||||||
:aria-label effective-aria-label
|
:aria-label effective-aria-label
|
||||||
:data-testid data-testid
|
|
||||||
:class (stl/css-case :switcher true
|
:class (stl/css-case :switcher true
|
||||||
:is-checked current-checked
|
:is-checked current-checked
|
||||||
:is-disabled disabled
|
:is-disabled disabled
|
||||||
@ -92,8 +95,12 @@
|
|||||||
:switcher--lg (= size :lg))
|
:switcher--lg (= size :lg))
|
||||||
:on-click handle-toggle
|
:on-click handle-toggle
|
||||||
:on-key-down handle-keydown}
|
:on-key-down handle-keydown}
|
||||||
[:div {:class (stl/css :switcher-track)}
|
[:div {:class (stl/css-case :switcher-track true
|
||||||
[:div {:class (stl/css :switcher-thumb)}]]]]))
|
:is-checked current-checked
|
||||||
|
:is-disabled disabled)}
|
||||||
|
[:div {:class (stl/css-case :switcher-thumb true
|
||||||
|
:is-checked current-checked
|
||||||
|
:is-disabled disabled)}]]]]))
|
||||||
|
|
||||||
;; Export as default
|
;; Export as default
|
||||||
(def switcher switcher*)
|
(def switcher switcher*)
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
import { Canvas, Meta } from '@storybook/blocks';
|
import { Canvas, Meta } from '@storybook/blocks';
|
||||||
import * as SwitcherStories from "./switcher.stories";
|
import * as SwitcherStories from "./switcher.stories";
|
||||||
|
|
||||||
<Meta title="DS/Controls/Switcher" />
|
<Meta title="Controls/Switcher" />
|
||||||
|
|
||||||
# Switcher
|
# Switcher
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ The switcher component consists of three main parts:
|
|||||||
## Props
|
## Props
|
||||||
|
|
||||||
| Name | Type | Default | Description | Required |
|
| Name | Type | Default | Description | Required |
|
||||||
|------|------|---------|-------------|----------|
|
| ---- | ---- | ------- | ----------- | -------- |
|
||||||
| `id` | `string` | auto-generated | Unique identifier for the switcher | No |
|
| `id` | `string` | auto-generated | Unique identifier for the switcher | No |
|
||||||
| `label` | `string` | - | Text label displayed next to the switcher | No |
|
| `label` | `string` | - | Text label displayed next to the switcher | No |
|
||||||
| `checked` | `boolean` | - | Controlled checked state | No |
|
| `checked` | `boolean` | - | Controlled checked state | No |
|
||||||
@ -75,7 +75,7 @@ The switcher supports three size variants: `"sm"`, `"md"` (default), and `"lg"`.
|
|||||||
|
|
||||||
### Disabled State
|
### Disabled State
|
||||||
|
|
||||||
Disabled switchers are non-interactive and visually distinct.
|
Disabled switchers are non-interactive and visually distinct. You can control the disabled state using the `disabled` prop in the controls panel.
|
||||||
|
|
||||||
```clj
|
```clj
|
||||||
[:> switcher* {:label "Disabled feature"
|
[:> switcher* {:label "Disabled feature"
|
||||||
@ -83,8 +83,6 @@ Disabled switchers are non-interactive and visually distinct.
|
|||||||
:default-checked false}]
|
:default-checked false}]
|
||||||
```
|
```
|
||||||
|
|
||||||
<Canvas of={SwitcherStories.Disabled} />
|
|
||||||
|
|
||||||
### Without Visible Label
|
### Without Visible Label
|
||||||
|
|
||||||
When no visible label is provided, use `aria-label` for accessibility.
|
When no visible label is provided, use `aria-label` for accessibility.
|
||||||
@ -96,22 +94,6 @@ When no visible label is provided, use `aria-label` for accessibility.
|
|||||||
|
|
||||||
<Canvas of={SwitcherStories.WithoutVisibleLabel} />
|
<Canvas of={SwitcherStories.WithoutVisibleLabel} />
|
||||||
|
|
||||||
## Accessibility
|
|
||||||
|
|
||||||
The switcher component follows accessibility best practices:
|
|
||||||
|
|
||||||
- **Role**: Uses `role="switch"` for proper semantic meaning
|
|
||||||
- **Keyboard Support**:
|
|
||||||
- `Tab` to focus the switcher
|
|
||||||
- `Space` or `Enter` to toggle the state
|
|
||||||
- **ARIA Attributes**:
|
|
||||||
- `aria-checked` reflects the current state
|
|
||||||
- `aria-disabled` when disabled
|
|
||||||
- `aria-label` for screen readers when no visible label
|
|
||||||
- **Focus Management**: Visible focus ring for keyboard navigation
|
|
||||||
|
|
||||||
<Canvas of={SwitcherStories.Accessibility} />
|
|
||||||
|
|
||||||
## Design Tokens
|
## Design Tokens
|
||||||
|
|
||||||
The switcher component uses the following design system tokens:
|
The switcher component uses the following design system tokens:
|
||||||
@ -119,11 +101,9 @@ The switcher component uses the following design system tokens:
|
|||||||
### Colors
|
### Colors
|
||||||
- **OFF state track**: `--color-background-quaternary` (muted gray)
|
- **OFF state track**: `--color-background-quaternary` (muted gray)
|
||||||
- **ON state track**: `--color-accent-success` (green/teal)
|
- **ON state track**: `--color-accent-success` (green/teal)
|
||||||
- **OFF state thumb**: `--color-foreground-secondary` (gray)
|
- **Thumb**: `--color-background-primary` (white/light)
|
||||||
- **ON state thumb**: `--color-foreground-primary` (white in dark theme, black in light theme)
|
|
||||||
- **Hover state thumb**: `--color-foreground-primary` (contrasting color)
|
|
||||||
- **Label**: `--color-foreground-primary`
|
- **Label**: `--color-foreground-primary`
|
||||||
- **Focus ring**: `--color-accent-tertiary` (teal in dark theme, purple in light theme)
|
- **Focus ring**: `--color-accent-primary`
|
||||||
|
|
||||||
### Sizes
|
### Sizes
|
||||||
- **Small**: Track 32×16px, Thumb 12px
|
- **Small**: Track 32×16px, Thumb 12px
|
||||||
|
|||||||
@ -27,12 +27,30 @@ $switcher-transition-duration: 0.2s;
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--sp-s);
|
gap: var(--sp-s);
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
// Focus ring using DS tokens - on wrapper cascading to track
|
||||||
|
&:focus-visible {
|
||||||
|
.switcher-track {
|
||||||
|
outline: $b-2 solid var(--color-accent-primary);
|
||||||
|
outline-offset: $b-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.switcher-label {
|
.switcher-label {
|
||||||
color: var(--title-foreground-color);
|
color: var(--color-foreground-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switcher-label.is-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.switcher {
|
.switcher {
|
||||||
@ -44,42 +62,7 @@ $switcher-transition-duration: 0.2s;
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
.switcher-track {
|
|
||||||
outline: $b-2 solid var(--color-accent-tertiary);
|
|
||||||
outline-offset: $b-2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disabled state
|
|
||||||
&.is-disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
|
|
||||||
.switcher-label {
|
|
||||||
cursor: not-allowed;
|
|
||||||
color: var(--text-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.switcher-track {
|
|
||||||
background-color: var(--color-background-tertiary);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-checked .switcher-track {
|
|
||||||
background-color: var(--color-background-quaternary);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switcher-thumb {
|
|
||||||
background-color: var(--color-foreground-secondary);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-checked .switcher-thumb {
|
|
||||||
background-color: var(--color-foreground-secondary);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size variants - Medium (default)
|
// Size variants - Medium (default)
|
||||||
&.switcher--md {
|
&.switcher--md {
|
||||||
@ -179,6 +162,31 @@ $switcher-transition-duration: 0.2s;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flat modifier-based selectors for states
|
||||||
|
.switcher.is-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switcher-track.is-disabled {
|
||||||
|
background-color: var(--color-background-tertiary);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switcher-track.is-disabled.is-checked {
|
||||||
|
background-color: var(--color-background-quaternary);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switcher-thumb.is-disabled {
|
||||||
|
background-color: var(--color-foreground-secondary);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switcher-thumb.is-disabled.is-checked {
|
||||||
|
background-color: var(--color-foreground-secondary);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.switcher-track,
|
.switcher-track,
|
||||||
.switcher-thumb {
|
.switcher-thumb {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import Components from "@target/components";
|
|||||||
const { Switcher } = Components;
|
const { Switcher } = Components;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "DS/Controls/Switcher",
|
title: "Controls/Switcher",
|
||||||
component: Components.Switcher,
|
component: Components.Switcher,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
checked: {
|
checked: {
|
||||||
@ -87,50 +87,6 @@ export const Controlled = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Sizes = {
|
|
||||||
render: () => (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
||||||
<Switcher
|
|
||||||
size="sm"
|
|
||||||
label="Small switcher"
|
|
||||||
defaultChecked={false}
|
|
||||||
data-testid="small-switcher"
|
|
||||||
/>
|
|
||||||
<Switcher
|
|
||||||
size="md"
|
|
||||||
label="Medium switcher"
|
|
||||||
defaultChecked={true}
|
|
||||||
data-testid="medium-switcher"
|
|
||||||
/>
|
|
||||||
<Switcher
|
|
||||||
size="lg"
|
|
||||||
label="Large switcher"
|
|
||||||
defaultChecked={false}
|
|
||||||
data-testid="large-switcher"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Disabled = {
|
|
||||||
render: () => (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
||||||
<Switcher
|
|
||||||
label="Disabled (off)"
|
|
||||||
disabled={true}
|
|
||||||
defaultChecked={false}
|
|
||||||
data-testid="disabled-off"
|
|
||||||
/>
|
|
||||||
<Switcher
|
|
||||||
label="Disabled (on)"
|
|
||||||
disabled={true}
|
|
||||||
defaultChecked={true}
|
|
||||||
data-testid="disabled-on"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithLongLabel = {
|
export const WithLongLabel = {
|
||||||
args: {
|
args: {
|
||||||
label: "This is a very long label that demonstrates how the switcher component handles text wrapping and layout when the label content is extensive",
|
label: "This is a very long label that demonstrates how the switcher component handles text wrapping and layout when the label content is extensive",
|
||||||
@ -153,33 +109,6 @@ export const WithoutVisibleLabel = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Accessibility = {
|
|
||||||
render: () => (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
||||||
<div>
|
|
||||||
<p style={{ marginBottom: "8px", fontSize: "14px", color: "#666" }}>
|
|
||||||
Try using Tab to focus and Space/Enter to toggle:
|
|
||||||
</p>
|
|
||||||
<Switcher
|
|
||||||
label="Keyboard accessible switcher"
|
|
||||||
defaultChecked={false}
|
|
||||||
data-testid="keyboard-switcher"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p style={{ marginBottom: "8px", fontSize: "14px", color: "#666" }}>
|
|
||||||
Screen reader accessible (no visible label):
|
|
||||||
</p>
|
|
||||||
<Switcher
|
|
||||||
aria-label="Toggle feature X"
|
|
||||||
defaultChecked={true}
|
|
||||||
data-testid="screen-reader-switcher"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Interactive = {
|
export const Interactive = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const [notifications, setNotifications] = React.useState(true);
|
const [notifications, setNotifications] = React.useState(true);
|
||||||
@ -211,7 +140,8 @@ export const Interactive = {
|
|||||||
<div style={{
|
<div style={{
|
||||||
marginTop: "16px",
|
marginTop: "16px",
|
||||||
padding: "12px",
|
padding: "12px",
|
||||||
backgroundColor: "#f5f5f5",
|
backgroundColor: "#f5f5f5",
|
||||||
|
color: "#000",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
fontSize: "14px"
|
fontSize: "14px"
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user