Skip to content
Closed
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,39 @@ export const Types = {
}
}
}

const PRIVACY_SCRIPT = [
IMPORT,
"import { ref } from 'vue'",
'',
'const off = ref(false)',
'const on = ref(true)'
]

const PRIVACY_TEMPLATE = `<div class="flex items-center gap-[var(--spacing-4)]">
<Switch v-model="off" kind="privacy" aria-label="Privacy off" />
<Switch v-model="on" kind="privacy" aria-label="Privacy on" />
</div>`

/** @type {import('@storybook/vue3').StoryObj<typeof Switch>} */
export const Privacy = {
render: () => ({
components: { Switch },
setup() {
const off = ref(false)
const on = ref(true)
return { off, on }
},
template: PRIVACY_TEMPLATE
}),
parameters: {
docs: {
controls: { disable: true },
description: {
story:
'Privacy variant in both states. The off handle carries a closed lock (`pi-lock`) tinted `var(--bg-surface)` against the muted handle; the on handle carries an open lock (`pi-lock-open`) tinted `var(--text-default)` against the canvas handle — so the icon contrasts against the handle rather than blending into the surrounding `var(--success-contrast)` track.'
},
source: { code: toSfc(PRIVACY_SCRIPT, PRIVACY_TEMPLATE) }
}
}
}
43 changes: 43 additions & 0 deletions packages/webkit/src/components/inputs/switch/switch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,49 @@ describe('Switch', () => {
})
})

describe('privacy lock icon', () => {
it('renders no lock icon when kind=default', () => {
const { getByTestId } = render(Switch, {
props: { kind: 'default', modelValue: true },
attrs: { 'aria-label': 'Toggle setting' }
})

const handle = getByTestId('input-switch__handle')
expect(handle.querySelector('i.pi')).toBeNull()
})

it('renders pi-lock tinted var(--bg-surface) when privacy is off', () => {
const { getByTestId } = render(Switch, {
props: { kind: 'privacy', modelValue: false },
attrs: { 'aria-label': 'Toggle setting' }
})

const icon = getByTestId('input-switch__handle').querySelector('i.pi') as HTMLElement | null
expect(icon).not.toBeNull()
expect(icon!.classList.contains('pi-lock')).toBe(true)
expect(icon!.classList.contains('pi-lock-open')).toBe(false)
expect(icon!.className).toContain('text-[var(--bg-surface)]')
expect(icon!.getAttribute('aria-hidden')).toBe('true')
})

it('renders pi-lock-open tinted var(--text-default) when privacy is on (contrasts against handle, not track)', () => {
const { getByTestId } = render(Switch, {
props: { kind: 'privacy', modelValue: true },
attrs: { 'aria-label': 'Toggle setting' }
})

const icon = getByTestId('input-switch__handle').querySelector('i.pi') as HTMLElement | null
expect(icon).not.toBeNull()
expect(icon!.classList.contains('pi-lock-open')).toBe(true)
expect(icon!.classList.contains('pi-lock')).toBe(false)
// The on icon must NOT reuse --success-contrast (the surrounding track color)
// — that made the icon "blend" into the track visually. It uses --text-default
// so it contrasts against the handle's --bg-canvas fill in both themes.
expect(icon!.className).toContain('text-[var(--text-default)]')
expect(icon!.className).not.toContain('text-[var(--success-contrast)]')
})
})

describe('smoke over type variants', () => {
it.each([['default'], ['privacy']] as const)('renders data-kind=%s', (type) => {
const { getByTestId } = render(Switch, {
Expand Down
2 changes: 1 addition & 1 deletion packages/webkit/src/components/inputs/switch/switch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@

const LOCK_ICON_CLASS = 'pi text-body-xxs leading-none'
const LOCK_OFF_CLASS = `${LOCK_ICON_CLASS} pi-lock text-[var(--bg-surface)]`
const LOCK_ON_CLASS = `${LOCK_ICON_CLASS} pi-lock-open text-[var(--success-contrast)]`
const LOCK_ON_CLASS = `${LOCK_ICON_CLASS} pi-lock-open text-[var(--text-default)]`

const rootClass = computed(() => cn(ROOT_CLASS, attrs.class as string | undefined))
</script>
Expand Down
Loading