Skip to content

Commit 0d3ea09

Browse files
abbeyperiniknowlerdanielroeautofix-ci[bot]
authored
fix: settings toggle should use input (#1049)
Co-authored-by: Nathan Knowler <nathan@knowler.dev> Co-authored-by: Daniel Roe <daniel@roe.dev> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent ad306be commit 0d3ea09

File tree

3 files changed

+235
-91
lines changed

3 files changed

+235
-91
lines changed

app/components/Settings/Toggle.client.vue

Lines changed: 107 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import TooltipApp from '~/components/Tooltip/App.vue'
33
44
const props = withDefaults(
55
defineProps<{
6-
label?: string
6+
label: string
77
description?: string
8-
class?: string
98
justify?: 'between' | 'start'
109
tooltip?: string
1110
tooltipPosition?: 'top' | 'bottom' | 'left' | 'right'
@@ -20,45 +19,47 @@ const props = withDefaults(
2019
)
2120
2221
const checked = defineModel<boolean>({
23-
default: false,
22+
required: true,
2423
})
24+
const id = useId()
2525
</script>
2626

2727
<template>
28-
<button
29-
type="button"
30-
class="w-full flex items-center gap-4 group focus-visible:outline-none py-1 -my-1"
31-
:class="[justify === 'start' ? 'justify-start' : 'justify-between', $props.class]"
32-
role="switch"
33-
:aria-checked="checked"
34-
@click="checked = !checked"
28+
<label
29+
:for="id"
30+
class="grid items-center gap-4 py-1 -my-1 grid-cols-[auto_1fr_auto]"
31+
:class="[justify === 'start' ? 'justify-start' : '']"
32+
:style="
33+
props.reverseOrder
34+
? 'grid-template-areas: \'toggle . label-text\''
35+
: 'grid-template-areas: \'label-text . toggle\''
36+
"
3537
>
3638
<template v-if="props.reverseOrder">
37-
<span
38-
class="inline-flex items-center h-6 w-11 shrink-0 rounded-full border p-0.25 transition-colors duration-200 shadow-sm ease-in-out motion-reduce:transition-none group-focus-visible:(outline-accent/70 outline-offset-2 outline-solid)"
39-
:class="
40-
checked
41-
? 'bg-accent border-accent group-hover:bg-accent/80'
42-
: 'bg-fg/50 border-fg/50 group-hover:bg-fg/70'
43-
"
44-
aria-hidden="true"
45-
>
46-
<span
47-
class="block h-5 w-5 rounded-full bg-bg shadow-sm transition-transform duration-200 ease-in-out motion-reduce:transition-none"
48-
/>
49-
</span>
39+
<input
40+
role="switch"
41+
type="checkbox"
42+
:id
43+
v-model="checked"
44+
class="toggle appearance-none h-6 w-11 rounded-full border border-fg relative shrink-0 bg-fg-subtle checked:bg-fg checked:border-fg focus-visible:(outline-2 outline-fg outline-offset-2) before:content-[''] before:absolute before:h-5 before:w-5 before:top-1px before:rounded-full before:bg-bg"
45+
style="grid-area: toggle"
46+
/>
5047
<TooltipApp
5148
v-if="tooltip && label"
5249
:text="tooltip"
5350
:position="tooltipPosition ?? 'top'"
5451
:to="tooltipTo"
5552
:offset="tooltipOffset"
5653
>
57-
<span class="text-sm text-fg font-medium text-start">
54+
<span class="text-sm text-fg font-medium text-start" style="grid-area: label-text">
5855
{{ label }}
5956
</span>
6057
</TooltipApp>
61-
<span v-else-if="label" class="text-sm text-fg font-medium text-start">
58+
<span
59+
v-else-if="label"
60+
class="text-sm text-fg font-medium text-start"
61+
style="grid-area: label-text"
62+
>
6263
{{ label }}
6364
</span>
6465
</template>
@@ -70,83 +71,109 @@ const checked = defineModel<boolean>({
7071
:to="tooltipTo"
7172
:offset="tooltipOffset"
7273
>
73-
<span class="text-sm text-fg font-medium text-start">
74+
<span class="text-sm text-fg font-medium text-start" style="grid-area: label-text">
7475
{{ label }}
7576
</span>
7677
</TooltipApp>
77-
<span v-else-if="label" class="text-sm text-fg font-medium text-start">
78-
{{ label }}
79-
</span>
8078
<span
81-
class="inline-flex items-center h-6 w-11 shrink-0 rounded-full border p-0.25 transition-colors duration-200 shadow-sm ease-in-out motion-reduce:transition-none group-focus-visible:(outline-accent/70 outline-offset-2 outline-solid)"
82-
:class="
83-
checked
84-
? 'bg-accent border-accent group-hover:bg-accent/80'
85-
: 'bg-fg/50 border-fg/50 group-hover:bg-fg/70'
86-
"
87-
aria-hidden="true"
79+
v-else-if="label"
80+
class="text-sm text-fg font-medium text-start"
81+
style="grid-area: label-text"
8882
>
89-
<span
90-
class="block h-5 w-5 rounded-full bg-bg shadow-sm transition-transform duration-200 ease-in-out motion-reduce:transition-none"
91-
/>
83+
{{ label }}
9284
</span>
85+
<input
86+
role="switch"
87+
type="checkbox"
88+
:id
89+
v-model="checked"
90+
class="toggle appearance-none h-6 w-11 rounded-full border border-fg relative shrink-0 bg-fg-subtle checked:bg-fg checked:border-fg focus-visible:(outline-2 outline-fg outline-offset-2) before:content-[''] before:absolute before:h-5 before:w-5 before:top-1px before:rounded-full before:bg-bg"
91+
style="grid-area: toggle; justify-self: end"
92+
/>
9393
</template>
94-
</button>
94+
</label>
9595
<p v-if="description" class="text-sm text-fg-muted mt-2">
9696
{{ description }}
9797
</p>
9898
</template>
9999

100100
<style scoped>
101-
/* Default order: label first, toggle last */
102-
button[aria-checked='false'] > span:last-of-type > span {
103-
translate: 0;
101+
/* Thumb position: logical property for RTL support */
102+
.toggle::before {
103+
inset-inline-start: 1px;
104104
}
105-
button[aria-checked='true'] > span:last-of-type > span {
106-
translate: calc(100%);
105+
106+
/* Track transition */
107+
.toggle {
108+
transition:
109+
background-color 200ms ease-in-out,
110+
border-color 100ms ease-in-out;
107111
}
108-
html[dir='rtl'] button[aria-checked='true'] > span:last-of-type > span {
109-
translate: calc(-100%);
112+
113+
.toggle::before {
114+
transition:
115+
background-color 200ms ease-in-out,
116+
translate 200ms ease-in-out;
110117
}
111118
112-
/* Reverse order: toggle first, label last */
113-
button[aria-checked='false'] > span:first-of-type > span {
114-
translate: 0;
119+
/* Hover states */
120+
.toggle:hover:not(:checked) {
121+
background: var(--fg-muted);
115122
}
116-
button[aria-checked='true'] > span:first-of-type > span {
117-
translate: calc(100%);
123+
124+
.toggle:checked:hover {
125+
background: var(--fg-muted);
126+
border-color: var(--fg-muted);
118127
}
119-
html[dir='rtl'] button[aria-checked='true'] > span:first-of-type > span {
120-
translate: calc(-100%);
128+
129+
/* RTL-aware checked thumb position */
130+
:dir(ltr) .toggle:checked::before {
131+
translate: 20px;
132+
}
133+
134+
:dir(rtl) .toggle:checked::before {
135+
translate: -20px;
121136
}
122137
138+
@media (prefers-reduced-motion: reduce) {
139+
.toggle,
140+
.toggle::before {
141+
transition: none;
142+
}
143+
}
144+
145+
/* Support forced colors */
123146
@media (forced-colors: active) {
124-
/* make toggle tracks and thumb visible in forced colors. */
125-
button[role='switch'] {
126-
& > span:last-of-type,
127-
& > span:first-of-type {
128-
forced-color-adjust: none;
129-
}
130-
131-
&[aria-checked='false'] > span:last-of-type,
132-
&[aria-checked='false'] > span:first-of-type {
133-
background: Canvas;
134-
border-color: CanvasText;
135-
136-
& > span {
137-
background: CanvasText;
138-
}
139-
}
140-
141-
&[aria-checked='true'] > span:last-of-type,
142-
&[aria-checked='true'] > span:first-of-type {
143-
background: Highlight;
144-
border-color: Highlight;
145-
146-
& > span {
147-
background: HighlightText;
148-
}
149-
}
147+
label > span {
148+
background: Canvas;
149+
color: Highlight;
150+
forced-color-adjust: none;
151+
}
152+
153+
label:has(.toggle:checked) > span {
154+
background: Highlight;
155+
color: Canvas;
156+
}
157+
158+
.toggle::before {
159+
forced-color-adjust: none;
160+
background-color: Highlight;
161+
}
162+
163+
.toggle,
164+
.toggle:hover {
165+
background: Canvas;
166+
border-color: CanvasText;
167+
}
168+
169+
.toggle:checked,
170+
.toggle:checked:hover {
171+
background: Highlight;
172+
border-color: CanvasText;
173+
}
174+
175+
.toggle:checked::before {
176+
background: Canvas;
150177
}
151178
}
152179
</style>
Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,134 @@
11
<script setup lang="ts">
2-
defineProps<{
3-
label?: string
4-
description?: string
5-
}>()
2+
const props = withDefaults(
3+
defineProps<{
4+
label: string
5+
description?: string
6+
justify?: 'between' | 'start'
7+
reverseOrder?: boolean
8+
}>(),
9+
{
10+
justify: 'between',
11+
reverseOrder: false,
12+
},
13+
)
614
</script>
715

816
<template>
9-
<div class="w-full flex items-center justify-between gap-4 py-1 -my-1">
10-
<span v-if="label" class="text-sm text-fg font-medium text-start">
11-
{{ label }}
12-
</span>
13-
<SkeletonBlock class="h-6 w-11 shrink-0 rounded-full" />
17+
<div
18+
class="grid items-center gap-4 py-1 -my-1 grid-cols-[auto_1fr_auto]"
19+
:class="[justify === 'start' ? 'justify-start' : '']"
20+
:style="
21+
props.reverseOrder
22+
? 'grid-template-areas: \'toggle . label-text\''
23+
: 'grid-template-areas: \'label-text . toggle\''
24+
"
25+
>
26+
<template v-if="props.reverseOrder">
27+
<SkeletonBlock class="h-6 w-11 shrink-0 rounded-full" style="grid-area: toggle" />
28+
<span
29+
v-if="label"
30+
class="text-sm text-fg font-medium text-start"
31+
style="grid-area: label-text"
32+
>
33+
{{ label }}
34+
</span>
35+
</template>
36+
<template v-else>
37+
<span
38+
v-if="label"
39+
class="text-sm text-fg font-medium text-start"
40+
style="grid-area: label-text"
41+
>
42+
{{ label }}
43+
</span>
44+
<SkeletonBlock
45+
class="h-6 w-11 shrink-0 rounded-full"
46+
style="grid-area: toggle; justify-self: end"
47+
/>
48+
</template>
1449
</div>
1550
<p v-if="description" class="text-sm text-fg-muted mt-2">
1651
{{ description }}
1752
</p>
1853
</template>
54+
55+
<style scoped>
56+
/* Thumb position: logical property for RTL support */
57+
.toggle::before {
58+
inset-inline-start: 1px;
59+
}
60+
61+
/* Track transition */
62+
.toggle {
63+
transition:
64+
background-color 200ms ease-in-out,
65+
border-color 100ms ease-in-out;
66+
}
67+
68+
.toggle::before {
69+
transition:
70+
background-color 200ms ease-in-out,
71+
translate 200ms ease-in-out;
72+
}
73+
74+
/* Hover states */
75+
.toggle:hover:not(:checked) {
76+
background: var(--fg-muted);
77+
}
78+
79+
.toggle:checked:hover {
80+
background: var(--fg-muted);
81+
border-color: var(--fg-muted);
82+
}
83+
84+
/* RTL-aware checked thumb position */
85+
:dir(ltr) .toggle:checked::before {
86+
translate: 20px;
87+
}
88+
89+
:dir(rtl) .toggle:checked::before {
90+
translate: -20px;
91+
}
92+
93+
@media (prefers-reduced-motion: reduce) {
94+
.toggle,
95+
.toggle::before {
96+
transition: none;
97+
}
98+
}
99+
100+
/* Support forced colors */
101+
@media (forced-colors: active) {
102+
label > span {
103+
background: Canvas;
104+
color: Highlight;
105+
forced-color-adjust: none;
106+
}
107+
108+
label:has(.toggle:checked) > span {
109+
background: Highlight;
110+
color: Canvas;
111+
}
112+
113+
.toggle::before {
114+
forced-color-adjust: none;
115+
background-color: Highlight;
116+
}
117+
118+
.toggle,
119+
.toggle:hover {
120+
background: Canvas;
121+
border-color: CanvasText;
122+
}
123+
124+
.toggle:checked,
125+
.toggle:checked:hover {
126+
background: Highlight;
127+
border-color: CanvasText;
128+
}
129+
130+
.toggle:checked::before {
131+
background: Canvas;
132+
}
133+
}
134+
</style>

0 commit comments

Comments
 (0)