Skip to content
Merged
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
17 changes: 13 additions & 4 deletions packages/ui/src/Input.recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@ const transitionCommon =
*
* Focus matches both native `:focus-visible` (plain inputs; browsers treat any
* focus in a text field as focus-visible) and react-aria's `data-focused`
* (inputs inside RAC TextField). Focus is declared after invalid so a focused
* invalid field shows the focus ring, as in Chakra.
* (inputs inside RAC TextField).
*
* Hover, invalid and focus all set `borderColor`, so their precedence has to be
* hover < invalid < focus. Declaration order will not buy that: Panda sorts a
* recipe's state rules itself, ranking selectors against a fixed
* link/visited/focus/hover/active table, which puts `_hover` *after* focus and
* after anything the table doesn't mention (`[data-invalid]`). Equal-specificity
* rules then leave hover winning. So the ladder is spelled with repeated `&`
* instead — `&&` and `&&&` emit `.input.input` and `.input.input.input`, making
* precedence specificity rather than order, which nothing downstream can
* resort. Variants still override freely; they land in a later cascade layer.
*
* Registered in the base preset (base-preset.ts), which also has the
* `staticCss` entry that keeps the runtime-prop size variants generated.
Expand All @@ -39,11 +48,11 @@ export const input = defineRecipe({
bg: "inherit",
color: "inherit",
_hover: { borderColor: "gray.300" },
"&[data-invalid], &:user-invalid": {
"&&:is([data-invalid], :user-invalid)": {
borderColor: "danger.500",
boxShadow: "0 0 0 1px token(colors.danger.500)",
},
"&:is(:focus-visible, [data-focused])": {
"&&&:is(:focus-visible, [data-focused])": {
zIndex: 1,
borderColor: "focusBorder",
boxShadow: "0 0 0 1px token(colors.focusBorder)",
Expand Down
11 changes: 7 additions & 4 deletions packages/ui/src/Select.recipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,12 @@ export const select = defineSlotRecipe({
// `> &` rather than a descendant selector, so an app's own invalid form
// wrapper cannot paint every control inside it red.
//
// Declared after hover and before focus so red beats a hover tint and
// the focus ring beats red, as in the input recipe.
"[data-invalid] > &": {
// Doubled `&` for the same reason as the input recipe: hover, invalid and
// focus all set `borderColor`, and Panda sorts state rules by its own
// pseudo-class table rather than declaration order, so hover would win
// these ties. The repeated `&` makes the hover < invalid < focus ladder a
// matter of specificity instead.
"[data-invalid] > &&": {
borderColor: "danger.500",
boxShadow: "0 0 0 1px token(colors.danger.500)",
},
Expand All @@ -98,7 +101,7 @@ export const select = defineSlotRecipe({
// focus moves to an option (aria-activedescendant) — which strips RAC's
// attribute for as long as the list has an active option, real focus
// never having left. Select's trigger holds no input, so it can't match.
"&[data-focus-visible], &:has(input:focus)": {
"&&&[data-focus-visible], &&&:has(input:focus)": {
boxShadow: "0 0 0 1px token(colors.focusBorder)",
borderColor: "focusBorder",
outline: "2px solid transparent",
Expand Down