-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(Stepper): define the public context boundary and deprecate useStepperContext #6074
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| --- | ||
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [component] Deprecate `useStepperContext` and narrow `StepperContextValue` to | ||
| the supported subset. Stepper's private coordination — the connector-fill | ||
| choreography (`previousActiveStep`) and the dev-mode step registry | ||
| (`registerStep`) — is no longer named on the public interface, so changing it | ||
| can no longer break consumer types. Both names stay exported until the next | ||
| major. | ||
|
|
||
| Custom step composition is not, and was not, supported through this hook: a | ||
| Stepper builds its context entirely from the props you passed it, so the hook | ||
| returns nothing the call site already lacks, and a hand-rolled step still | ||
| cannot draw a correct connector track. Compose with `<Step>` and its | ||
| `children`, `indicator`, and `endContent` slots, and gate step content on the | ||
| same state that drives `activeStep`. | ||
|
|
||
| @alif416 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| // Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|
|
||
| /** | ||
| * @file Stepper.public.test.ts | ||
| * @input Imports the Stepper barrel and its context module | ||
| * @output Locks the supported public context surface | ||
| * @position Compatibility test guarding @astryxdesign/core/Stepper | ||
| * | ||
| * The decision this file encodes: `useStepperContext` and | ||
| * `StepperContextValue` are NOT a supported extension point. Custom step | ||
| * composition goes through `<Step>` and its slots. Both names stay exported | ||
| * until the next major so the removal lands at an explicit compatibility | ||
| * boundary, and until then they must not carry Stepper's private | ||
| * coordination — see StepperContext.ts. | ||
| * | ||
| * The `expectTypeOf` assertions here are enforced by `pnpm -F | ||
| * @astryxdesign/core typecheck`, not by the test run — a failing one surfaces | ||
| * as `TS2554: Expected 2 arguments, but got 1` on the assertion's line, which | ||
| * is how vitest reports a type assertion that did not hold. Verified by | ||
| * re-widening `StepperContextValue` and watching exactly the matching lines | ||
| * fail. | ||
| */ | ||
|
|
||
| import {readFile} from 'node:fs/promises'; | ||
| import {resolve} from 'node:path'; | ||
|
|
||
| import {describe, expect, expectTypeOf, it} from 'vitest'; | ||
|
|
||
| // Type-only: the runtime surface is checked through a dynamic `import('./index')` | ||
| // below, so nothing here is needed as a value. | ||
| import type {useStepperContext, StepperContextValue} from './index'; | ||
| import type {StepperCoordination} from './StepperContext'; | ||
|
|
||
| describe('Stepper public context surface', () => { | ||
| it('keeps private coordination off the public interface', () => { | ||
| // The two fields that made an internal change a consumer type break: the | ||
| // connector-fill choreography and the dev-mode step registry. Neither is | ||
| // something a consumer configures, so neither is named out here. | ||
| expectTypeOf<StepperContextValue>().not.toHaveProperty( | ||
| 'previousActiveStep', | ||
| ); | ||
| expectTypeOf<StepperContextValue>().not.toHaveProperty('registerStep'); | ||
|
|
||
| // Guarded at the hook too, which is the declaration a consumer actually | ||
| // reads — widening the return type is the way this would regress. | ||
| expectTypeOf<ReturnType<typeof useStepperContext>>().not.toHaveProperty( | ||
| 'previousActiveStep', | ||
| ); | ||
| expectTypeOf<ReturnType<typeof useStepperContext>>().not.toHaveProperty( | ||
| 'registerStep', | ||
| ); | ||
| }); | ||
|
|
||
| it('still names the fields the deprecation window promises', () => { | ||
| // Narrowing removed private coordination, not the supported reads. Anyone | ||
| // already calling the hook keeps compiling until the major. | ||
| expectTypeOf<StepperContextValue>().toHaveProperty('activeStep'); | ||
| expectTypeOf<StepperContextValue>().toHaveProperty('orientation'); | ||
| expectTypeOf<StepperContextValue>().toHaveProperty('isNonLinear'); | ||
| expectTypeOf<StepperContextValue>().toHaveProperty('onStepClick'); | ||
| expectTypeOf<StepperContextValue>().toHaveProperty('density'); | ||
| expectTypeOf<StepperContextValue>().toHaveProperty('indicatorPosition'); | ||
|
|
||
| expectTypeOf< | ||
| ReturnType<typeof useStepperContext> | ||
| >().toEqualTypeOf<StepperContextValue>(); | ||
| }); | ||
|
|
||
| it('carries one value on the wire, widened only inside the module', () => { | ||
| // The split is about which declaration a consumer can reach, not about | ||
| // building a second object per render. If these ever diverge structurally, | ||
| // the provider is handing Step something the public read cannot describe. | ||
| expectTypeOf<StepperCoordination>().toMatchTypeOf<StepperContextValue>(); | ||
| expectTypeOf<StepperCoordination>().toHaveProperty('previousActiveStep'); | ||
| expectTypeOf<StepperCoordination>().toHaveProperty('registerStep'); | ||
| }); | ||
|
|
||
| it('closes the internal seam by module boundary, not by naming', async () => { | ||
| // An `@internal` tag is a note to a reader; a builder reading an exported | ||
| // declaration finds every name on it and can reasonably wire one. So the | ||
| // coordination hook and its type must not reach the barrel at all. | ||
| const entry = await import('./index'); | ||
| expect(Object.keys(entry)).not.toContain('useStepperCoordination'); | ||
| expect(Object.keys(entry)).not.toContain('StepperContext'); | ||
|
|
||
| // Checked against the barrel's CODE, with comments stripped: the block | ||
| // documenting the deprecation names `StepperCoordination` in prose, and a | ||
| // raw substring check cannot tell that from an export. | ||
| const source = await readFile(resolve(__dirname, 'index.ts'), 'utf8'); | ||
| const code = source | ||
| .replace(/\/\*[\s\S]*?\*\//g, '') | ||
| .replace(/\/\/.*$/gm, ''); | ||
| expect(code).not.toContain('StepperCoordination'); | ||
| expect(code).not.toContain('useStepperCoordination'); | ||
| }); | ||
|
|
||
| it('still exports the deprecated names, so removal needs a major', async () => { | ||
| // The other half of the contract: this test fails if someone drops them in | ||
| // a patch instead of at the compatibility boundary. | ||
| const entry = await import('./index'); | ||
| expect(Object.keys(entry)).toContain('useStepperContext'); | ||
|
|
||
| const source = await readFile( | ||
| resolve(__dirname, 'StepperContext.ts'), | ||
| 'utf8', | ||
| ); | ||
| expect(source).toContain('@deprecated'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.