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
8 changes: 8 additions & 0 deletions .changeset/card-ring-overflow-clip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@chakra-ui/react": patch
"@chakra-ui/panda-preset": patch
---

Fix issue where the checked ring of `RadioCard` and `CheckboxCard` (outline
variant) gets clipped when a parent has `overflow: hidden|auto|scroll`. The ring
is now drawn with an inset shadow instead of an outer shadow.
6 changes: 6 additions & 0 deletions .changeset/chore-cli-migrate-get-tsconfig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@chakra-ui/cli": patch
---

Migrate from deprecated `tsconfck` to `get-tsconfig`. Enables installation next
to TypeScript version 6 and up.
5 changes: 5 additions & 0 deletions .changeset/cli-windows-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chakra-ui/cli": patch
---

Normalize resolved tsconfig paths to native separators on Windows
6 changes: 6 additions & 0 deletions .changeset/panda-preset-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@chakra-ui/panda-preset": minor
---

Sync theme from `@chakra-ui/react`: register the missing `floatingPanel` slot
recipe and update the date picker range-selection styles
51 changes: 51 additions & 0 deletions apps/www/content/docs/components/concepts/animation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,54 @@ it easy to create complex animations with multiple keyframes.
This is a composed animation
</Box>
```

## Reduced motion

The built-in component animations play regardless of the user's
`prefers-reduced-motion` setting. To disable the enter/exit animations for users
who prefer reduced motion, add this rule to your `globalCss` config:

```tsx
import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react"

const config = defineConfig({
globalCss: {
'[data-state="open"], [data-state="closed"]': {
_motionReduce: {
animationDuration: "1ms !important",
},
},
},
})

export const system = createSystem(defaultConfig, config)
```

This collapses the enter/exit animations of every disclosure component (dialog,
drawer, menu, popover, tooltip, etc.) to `1ms` when reduced motion is enabled.

A few details worth knowing:

- **Scope to `data-state="open"` and `data-state="closed"`** rather than
targeting all elements. A blanket rule also collapses looping animations
(`Spinner`, `Skeleton`, indeterminate `Progress`), turning them into a
flicker. Loading indicators are considered essential motion and are best left
running.
- **`animation: none` also works.** The presence logic detects it and unmounts
exiting components immediately. A `1ms` duration is suggested because it keeps
`animationend` events firing for any custom code that listens for them.
- **The `!important` is required** because the rule lives in a lower CSS layer
than component recipes.

When building custom animations, use the `_motionReduce` condition to provide a
reduced-motion alternative:

```tsx
<Box
data-state="open"
_open={{ animationName: "slide-from-bottom, fade-in" }}
_motionReduce={{ animationName: "fade-in" }}
>
Slides on screen, but only fades for reduced motion users
</Box>
```
103 changes: 97 additions & 6 deletions packages/cli/__tests__/resolve-tsconfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe("resolveTsconfig", () => {
expect(result).toBe(appTsconfigPath)
})

it("falls back to first reference when no paths found", async () => {
it("picks the reference that includes the source file when no paths exist", async () => {
writeFileSync(
join(testDir, "tsconfig.json"),
JSON.stringify({
Expand Down Expand Up @@ -157,7 +157,7 @@ describe("resolveTsconfig", () => {
expect(result).toBe(tsconfigPath)
})

it("picks the reference with paths when it is not the first one", async () => {
it("picks the matching reference regardless of reference order", async () => {
writeFileSync(
join(testDir, "tsconfig.json"),
JSON.stringify({
Expand Down Expand Up @@ -229,14 +229,14 @@ describe("resolveTsconfig", () => {
writeFileSync(join(testDir, "src/index.ts"), "export default {}")

const result = await resolveTsconfig(join(testDir, "src/index.ts"))
// tsconfck merges extends into the parsed tsconfig, so paths should be visible
// get-tsconfig merges extends into the parsed tsconfig, so paths should be visible
expect(result).toBe(appTsconfigPath)
})

it("picks non-first reference with inherited paths over first reference without", async () => {
// This test disambiguates: does tsconfck merge `extends` in referenced
// configs so our `paths` check finds inherited paths? If not, the fallback
// would wrongly pick tsconfig.node.json (listed first).
// The matched config inherits its `paths` via `extends`. parseTsconfig
// resolves the extends chain, so the returned config still exposes the
// inherited paths to downstream consumers (e.g. esbuild).
writeFileSync(
join(testDir, "tsconfig.json"),
JSON.stringify({
Expand Down Expand Up @@ -284,6 +284,97 @@ describe("resolveTsconfig", () => {
expect(result).toBe(appTsconfigPath)
})

it("falls back to the root config when no reference includes the source file", async () => {
const rootTsconfigPath = join(testDir, "tsconfig.json")
writeFileSync(
rootTsconfigPath,
JSON.stringify({
files: [],
references: [{ path: "./tsconfig.node.json" }],
}),
)

writeFileSync(
join(testDir, "tsconfig.node.json"),
JSON.stringify({
compilerOptions: {},
include: ["vite.config.ts"],
}),
)

writeFileSync(join(testDir, "src/index.ts"), "export default {}")

const result = await resolveTsconfig(join(testDir, "src/index.ts"))
expect(result).toBe(rootTsconfigPath)
})

it("resolves nested project references", async () => {
writeFileSync(
join(testDir, "tsconfig.json"),
JSON.stringify({
files: [],
references: [{ path: "./tsconfig.solution.json" }],
}),
)

// intermediate solution-style config referencing the app config
writeFileSync(
join(testDir, "tsconfig.solution.json"),
JSON.stringify({
files: [],
references: [{ path: "./tsconfig.app.json" }],
}),
)

const appTsconfigPath = join(testDir, "tsconfig.app.json")
writeFileSync(
appTsconfigPath,
JSON.stringify({
compilerOptions: {
paths: { "@/*": ["./src/*"] },
},
include: ["src"],
}),
)

writeFileSync(join(testDir, "src/index.ts"), "export default {}")

const result = await resolveTsconfig(join(testDir, "src/index.ts"))
expect(result).toBe(appTsconfigPath)
})

it("tolerates circular and missing references", async () => {
writeFileSync(
join(testDir, "tsconfig.json"),
JSON.stringify({
files: [],
references: [
// circular: points back to the root config
{ path: "./tsconfig.json" },
// missing: file does not exist
{ path: "./tsconfig.missing.json" },
{ path: "./tsconfig.app.json" },
],
}),
)

const appTsconfigPath = join(testDir, "tsconfig.app.json")
writeFileSync(
appTsconfigPath,
JSON.stringify({
compilerOptions: {
paths: { "@/*": ["./src/*"] },
},
include: ["src"],
}),
)

writeFileSync(join(testDir, "src/index.ts"), "export default {}")

const result = await resolveTsconfig(join(testDir, "src/index.ts"))
expect(result).toBe(appTsconfigPath)
})

it("finds tsconfig from deeply nested source file", async () => {
const tsconfigPath = join(testDir, "tsconfig.json")
writeFileSync(
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"debug": "^4.4.3",
"dotenv": "^17.2.3",
"esbuild": "^0.27.3",
"get-tsconfig": "^4.14.0",
"globby": "16.1.1",
"https-proxy-agent": "^7.0.6",
"look-it-up": "2.1.0",
Expand All @@ -69,7 +70,6 @@
"prettier": "3.8.1",
"recast": "^0.23.0",
"scule": "1.3.0",
"tsconfck": "^3.1.6",
"zod": "^3.25.76"
},
"peerDependencies": {
Expand Down
136 changes: 112 additions & 24 deletions packages/cli/src/utils/resolve-tsconfig.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,112 @@
import createDebug from "debug"
import { resolve } from "node:path"
import { parse } from "tsconfck"
import { createFilesMatcher, getTsconfig, parseTsconfig } from "get-tsconfig"
import type { TsConfigJsonResolved } from "get-tsconfig"
import { realpathSync, statSync } from "node:fs"
import { dirname, join, normalize, resolve } from "node:path"

const debug = createDebug("chakra:tsconfig")

type ConfigNode = {
path: string
config: TsConfigJsonResolved
references?: ConfigNode[]
}

// resolve relative path to referenced tsconfig
function resolveReferencePath(configPath: string, refPath: string): string {
const base = resolve(dirname(configPath), refPath)

try {
if (statSync(base).isFile()) return base
} catch {}

return join(base, "tsconfig.json")
}

// recursively load referenced tsconfigs
function loadReference(
parent: ConfigNode,
refPath: string,
seen: Set<string>,
): ConfigNode {
const configPath = resolveReferencePath(parent.path, refPath)

const real = realpathSync(configPath)
if (seen.has(real)) {
throw new Error(`Circular tsconfig reference: ${configPath}`)
}

const config = parseTsconfig(configPath)
const node: ConfigNode = {
path: configPath,
config,
}

// follow nested references
seen.add(real)
resolveReferences(node, seen)
seen.delete(real)

return node
}

// recursively resolve referenced tsconfigs
function resolveReferences(
node: ConfigNode,
seen = new Set<string>(),
): ConfigNode {
const { references } = node.config
if (!references?.length) return node

debug("solution-style tsconfig detected, checking references...")
node.references = references.flatMap((ref) => {
// Skip references that are missing, malformed, or circular so a single
// broken entry doesn't fail resolution for the whole project
if (!ref?.path) {
debug("invalid tsconfig reference in", node.path)
return []
}
try {
return [loadReference(node, ref.path, seen)]
} catch (error) {
debug("skipping unresolvable reference:", ref.path, error)
return []
}
})

return node
}

function findConfigForFile(root: ConfigNode, sourceFile: string): ConfigNode {
function visit(node: ConfigNode): ConfigNode | undefined {
// Explore children first so the first matching descendant wins.
for (const ref of node.references ?? []) {
const match = visit(ref)
if (match) return match
}

const matcher = createFilesMatcher(node)
return matcher(sourceFile) ? node : undefined
}

return visit(root) ?? root
}

function loadResolvedConfig(sourceFile: string): ConfigNode | undefined {
const root = getTsconfig(sourceFile)
if (!root) return undefined

const real = realpathSync(root.path)
const node: ConfigNode = {
path: root.path,
config: root.config,
}

const seen = new Set([real])
const resolved = resolveReferences(node, seen)
return findConfigForFile(resolved, sourceFile)
}

/**
* Resolves the correct tsconfig file for a given source file.
*
Expand All @@ -22,33 +125,18 @@ export async function resolveTsconfig(
}

try {
const result = await parse(sourceFile)
const nearest = loadResolvedConfig(sourceFile)

if (!result.tsconfigFile) {
if (!nearest) {
debug("no tsconfig found for", sourceFile)
return undefined
}

debug("found tsconfig:", result.tsconfigFile)

// If this is a solution-style tsconfig (has references, no real files),
// find the referenced config that contains `paths`
if (result.referenced && result.referenced.length > 0) {
debug("solution-style tsconfig detected, checking references...")

for (const ref of result.referenced) {
if (ref.tsconfig?.compilerOptions?.paths) {
debug("found paths in referenced tsconfig:", ref.tsconfigFile)
return ref.tsconfigFile
}
}

// Fall back to the first referenced tsconfig
debug("no paths found in references, using first reference")
return result.referenced[0].tsconfigFile
}

return result.tsconfigFile
// get-tsconfig returns forward-slash paths while reference resolution
// produces native paths. Normalize so output is native on every platform.
const configPath = normalize(nearest.path)
debug("found tsconfig:", configPath)
return configPath
} catch (error) {
debug("tsconfig resolution failed:", error)
return undefined
Expand Down
Loading
Loading