Skip to content

Commit 69bfc53

Browse files
authored
fix(focus-states): update cut-off focus states (#439)
1 parent 07426f6 commit 69bfc53

9 files changed

Lines changed: 37 additions & 16 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@cloudflare/kumo": patch
3+
---
4+
5+
Improve focus ring consistency and clipping behavior across inputs and related controls.
6+
7+
- Move the command palette focus ring to the input header container with `focus-within` and remove duplicate input-level ring styles.
8+
- Update `Select` trigger and option focus styles to use inset focus rings to prevent clipping in rounded/overflow contexts.
9+
- Fix clipboard copy button focus ring clipping by using inset focus-visible ring, matching border-radius inheritance, and isolated stacking.
10+
- Align `InputGroup` and `InputGroup.Button` focus ring color to `ring-kumo-focus`, including hybrid container-zone focus ring classes.
11+
- Update InputGroup tests to match inline focus ring class changes.
12+
- Set DatePicker (`react-day-picker`) focus ring token to `var(--color-kumo-brand)`.
13+
- Update InputGroup container and hybrid keyboard outlines in `kumo-binding.css` to use `var(--color-kumo-focus)` at 1px weight.

packages/kumo/src/components/clipboard-text/clipboard-text.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,11 @@ export const ClipboardText = forwardRef<HTMLDivElement, ClipboardTextProps>(
249249
ref={buttonRef}
250250
size={sizeConfig.buttonSize}
251251
variant="ghost"
252-
className="rounded-none border-l! border-kumo-line! px-3 relative overflow-hidden transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-kumo-focus/50 focus-visible:ring-2 focus-visible:ring-kumo-brand"
252+
className={cn(
253+
"rounded-l-none rounded-r-[inherit] border-l! border-kumo-line! px-3 relative isolate overflow-hidden transition-all duration-200",
254+
"focus:ring-inset focus:ring-kumo-focus/50",
255+
"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-kumo-brand",
256+
)}
253257
onClick={copyToClipboard}
254258
aria-label={copyAction}
255259
>

packages/kumo/src/components/command-palette/command-palette.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ function InputHeader({
251251
trailing?: React.ReactNode;
252252
}) {
253253
return (
254-
<div className="flex items-center gap-3 bg-kumo-base px-4 py-3">
254+
<div className="flex items-center gap-3 bg-kumo-base px-4 py-3 focus-within:ring-2 focus-within:ring-kumo-brand">
255255
{leading ?? (
256256
<MagnifyingGlassIcon
257257
className="h-4 w-4 text-kumo-subtle"
@@ -767,7 +767,7 @@ function PanelInput({
767767
placeholder={placeholder}
768768
className={cn(
769769
"flex-1 border-none bg-transparent text-base kumo-input-placeholder",
770-
"outline-none focus:ring-kumo-focus/50 focus:ring-[1.5px]",
770+
"outline-none",
771771
className,
772772
)}
773773
onKeyDown={handleKeyDown}

packages/kumo/src/components/input-group/input-group-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export const Button = forwardRef<
162162
// Focused element renders above hovered siblings for focus indicator
163163
"focus:z-[2] focus:border-kumo-line focus:outline focus:-outline-offset-1",
164164
// Suppress the base Button's focus ring so only our outline shows
165-
"focus-visible:ring-0 focus-visible:shadow-none",
165+
"focus-visible:ring-2 focus-visible:ring-kumo-focus",
166166
// Match the group's disabled visual treatment
167167
"disabled:bg-kumo-overlay disabled:text-kumo-inactive!",
168168
],

packages/kumo/src/components/input-group/input-group.test.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,7 +1170,7 @@ describe("InputGroup", () => {
11701170
expect(group.className).toContain("overflow-visible");
11711171
expect(group.className).toContain("ring-0");
11721172
expect(group.className).not.toContain("overflow-hidden");
1173-
expect(group.className).not.toContain("focus-within:ring-kumo-line");
1173+
expect(group.className).not.toContain("focus-within:ring-kumo-focus");
11741174
});
11751175

11761176
it("container mode gets shared focus ring classes", () => {
@@ -1184,7 +1184,7 @@ describe("InputGroup", () => {
11841184
"[data-slot='input-group']",
11851185
) as HTMLElement;
11861186
expect(group.className).toContain("overflow-hidden");
1187-
expect(group.className).toContain("focus-within:ring-kumo-line");
1187+
expect(group.className).toContain("focus-within:ring-kumo-focus");
11881188
expect(group.className).not.toContain("overflow-visible");
11891189
});
11901190

@@ -1206,7 +1206,7 @@ describe("InputGroup", () => {
12061206
expect(group.className).toContain("overflow-visible");
12071207
expect(group.className).toContain("ring-0");
12081208
expect(group.className).not.toContain("overflow-hidden");
1209-
expect(group.className).not.toContain("focus-within:ring-kumo-line");
1209+
expect(group.className).not.toContain("focus-within:ring-kumo-focus");
12101210
});
12111211
});
12121212

@@ -1305,6 +1305,8 @@ describe("InputGroup", () => {
13051305
expect(containerZone.className).toContain("border-kumo-line");
13061306
expect(containerZone.className).toContain("ring-0");
13071307
expect(containerZone.className).toContain("shadow-none");
1308+
expect(containerZone.className).toContain("focus-within:ring-1");
1309+
expect(containerZone.className).toContain("focus-within:ring-kumo-focus");
13081310
// Double-border prevention with adjacent individual-mode buttons
13091311
expect(containerZone.className).toContain("not-first:border-l-0");
13101312
// No focus-within ring — CSS outline in kumo-binding.css handles focus

packages/kumo/src/components/input-group/input-group.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,7 @@ const Root = forwardRef<
134134
? [
135135
"overflow-hidden",
136136
// Focus state must come AFTER inputVariants to override ring-kumo-line
137-
"focus-within:ring-kumo-line",
138-
// The CSS in kumo-binding.css handles the native outline
137+
"focus-within:ring-kumo-focus",
139138
]
140139
: // isolate creates a new stacking context so z-index in children doesn't leak out
141140
"isolate overflow-visible ring-0 shadow-none",
@@ -200,6 +199,7 @@ const Root = forwardRef<
200199
// so the zone matches adjacent individual-mode buttons exactly.
201200
"ring-0 shadow-none",
202201
"border border-kumo-line",
202+
"focus-within:ring-1 focus-within:ring-kumo-focus",
203203
// Collapse double borders between zone and adjacent individual-mode button
204204
"not-first:border-l-0",
205205
// Inherit border-radius from the outer container on outer edges only;

packages/kumo/src/components/select/select.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function selectVariants({
8080
return cn(
8181
buttonVariants({ size }),
8282
"justify-between font-normal",
83-
"focus:opacity-100 focus:ring-kumo-focus/50 *:in-focus:opacity-100",
83+
"focus:opacity-100 focus:ring-kumo-focus/50 focus-visible:ring-inset *:in-focus:opacity-100",
8484
);
8585
}
8686

@@ -546,7 +546,9 @@ function Option<T>({ children, value, disabled, className }: OptionProps<T>) {
546546
value={value}
547547
disabled={disabled}
548548
className={cn(
549-
"group mx-1.5 flex cursor-pointer items-center justify-between gap-2 rounded px-2 py-1.5 text-base outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand data-highlighted:bg-kumo-tint",
549+
"group mx-1.5 flex cursor-pointer items-center justify-between gap-2 rounded px-2 py-1.5 text-base outline-none",
550+
"focus-visible:z-50 focus-visible:ring-2 focus-visible:ring-kumo-brand focus-visible:ring-inset",
551+
"data-highlighted:bg-kumo-tint",
550552
"data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
551553
className,
552554
)}

packages/kumo/src/styles/kumo-binding.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,8 @@
232232
Uses vanilla CSS because Tailwind focus-visible: targets the focused element itself,
233233
but we need :has(:focus-visible) to outline the container when a descendant is focused. */
234234
[data-slot="input-group"][data-focus-mode="container"]:has(:focus-visible) {
235-
/* 2px solid matches the browser's default focus ring weight */
236-
outline: solid 2px -webkit-focus-ring-color;
235+
/* 1px solid matches the browser's default focus ring weight */
236+
outline: solid 1px var(--color-kumo-focus);
237237
}
238238

239239
/* InputGroup hybrid mode — keyboard outline for the container zone wrapper.
@@ -242,6 +242,6 @@
242242
[data-slot="input-group-container-zone"]:has(:focus-visible) {
243243
/* 1px to match the individual-mode outline weight (thinner than container's 2px
244244
because the zone is nested — a thick outline would overwhelm sibling buttons) */
245-
outline: solid 1px -webkit-focus-ring-color;
245+
outline: solid 1px var(--color-kumo-focus);
246246
outline-offset: -1px;
247247
}

packages/kumo/src/styles/kumo.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
--rdp-nav-border: oklch(14.5% 0 0 / 0.1);
105105
--rdp-hover-bg: oklch(90% 0 0);
106106
--rdp-fill-hover-bg: oklch(87% 0 0);
107-
--rdp-focus-ring: oklch(75% 0 0);
107+
--rdp-focus-ring: var(--color-kumo-brand);
108108

109109
position: relative;
110110
box-sizing: border-box;
@@ -127,7 +127,7 @@
127127
--rdp-nav-border: oklch(26.9% 0 0);
128128
--rdp-hover-bg: oklch(20% 0 0);
129129
--rdp-fill-hover-bg: oklch(25% 0 0);
130-
--rdp-focus-ring: oklch(45% 0 0);
130+
--rdp-focus-ring: var(--color-kumo-brand);
131131
}
132132

133133
.rdp-root[dir="rtl"] {

0 commit comments

Comments
 (0)