Skip to content

Commit 56d961f

Browse files
[wb1812.3.migratewb] Migrate Wonder Blocks off old id providers (#2391)
## Summary: This is the last piece in the first batch of work. This migrates all Wonder Blocks components off our old ID providers and onto the new `Id` component. There are also some documentation tweaks to make the deprecation clearer in our stories, since that's some primary documentation for folks. With this PR, we can cut a release and then begin updating consumer repos accordingly. First, to include this update, then to migrate them off the old ways and onto the new. ### Release process: Once this small stack of PRs are landed and released, the following PRs need to be updated with that release, then landed/deployed: - Perseus: Khan/perseus#2007 - Webapp - Khan/webapp#28105 - Khan/webapp#28127 Issue: WB-1812 ## Test plan: `yarn test` `yarn typecheck` `yarn start:storybook` Author: somewhatabstract Reviewers: jandrade, somewhatabstract Required Reviewers: Approved By: jandrade Checks: ⌛ Lint / Lint (ubuntu-latest, 20.x), ⌛ Check build sizes (ubuntu-latest, 20.x), ⌛ Chromatic - Build on regular PRs / chromatic (ubuntu-latest, 20.x), ⌛ Publish npm snapshot (ubuntu-latest, 20.x), ⌛ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ⏭️ Chromatic - Skip on Release PR (changesets), ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x), ✅ gerald, ⏭️ dependabot Pull Request URL: #2391
1 parent 897686b commit 56d961f

File tree

34 files changed

+259
-292
lines changed

34 files changed

+259
-292
lines changed

.changeset/witty-panthers-shave.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@khanacademy/wonder-blocks-search-field": major
3+
"@khanacademy/wonder-blocks-accordion": major
4+
"@khanacademy/wonder-blocks-dropdown": major
5+
"@khanacademy/wonder-blocks-popover": major
6+
"@khanacademy/wonder-blocks-testing": major
7+
"@khanacademy/wonder-blocks-tooltip": major
8+
"@khanacademy/wonder-blocks-switch": major
9+
"@khanacademy/wonder-blocks-modal": major
10+
"@khanacademy/wonder-blocks-form": major
11+
"@khanacademy/wonder-blocks-core": patch
12+
---
13+
14+
- Migrate Wonder Blocks components off old id providers and onto new `Id` component

__docs__/wonder-blocks-core/id.mdx

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as React from "react";
2+
import {Meta, Story, Canvas} from "@storybook/blocks";
3+
import * as IdStories from "./id.stories";
4+
5+
<Meta of={IdStories} />
6+
7+
# Id
8+
9+
`Id` is a component that provides an identifier to its children.
10+
11+
It is useful for situations where the `useId` hook cannot be easily used,
12+
such as in class-based components.
13+
14+
If an `id` prop is provided, that is passed through to the children;
15+
otherwise, a unique identifier is generated.
16+
17+
## Usage
18+
19+
```tsx
20+
import {Id} from "@khanacademy/wonder-blocks-core";
21+
22+
<Id id={maybeId}>{(id) => <div id={id}>Hello, world!</div>}</Id>;
23+
```
24+
25+
## Examples
26+
27+
### 1. Generating an id
28+
29+
An identifier will always be generated if an `id` prop is not provided, or the
30+
provided `id` property is falsy.
31+
32+
<Canvas withSource="open" of={IdStories.GeneratedIdExample} />
33+
34+
### 2. Passthrough an id
35+
36+
If an `id` prop is provided and it is truthy, that value will be passed through
37+
to the children.
38+
39+
<Canvas sourceState="shown" of={IdStories.PassedThroughIdExample} />

__docs__/wonder-blocks-core/use-unique-id.mdx

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import {Meta, Story, Canvas} from "@storybook/blocks";
2-
import * as UseUniqueIdStories from './use-unique-id.stories';
2+
import * as UseUniqueIdStories from "./use-unique-id.stories";
33

44
<Meta of={UseUniqueIdStories} />
55

66
# `useUniqueIdWithoutMock`
77

8+
DEPRECATED: Will be removed in a future release. Use `useId` from React or
9+
the `Id` component.
10+
811
This hook is similar to `<UniqueIDProvider mockOnFirstRender={false}>`.
912
It will return `null` on the initial render and then the same identifier
1013
factory for each subsequent render. The identifier factory is unique to
@@ -19,6 +22,9 @@ render tree.
1922

2023
# `useUniqueIdWithMock`
2124

25+
DEPRECATED: Will be removed in a future release. Use `useId` from React or
26+
the `Id` component.
27+
2228
This hook is similar to `<UniqueIDProvider mockOnFirstRender={true}>`.
2329
It will return a mock identifier factory on the initial render that doesn'that
2430
guarantee identifier uniqueness. Mock mode can help things appear on the screen

__docs__/wonder-blocks-timing/with-action-scheduler.stories.tsx

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
/* eslint-disable import/no-deprecated */
21
import * as React from "react";
32
import {Meta} from "@storybook/react";
4-
import {IDProvider, View} from "@khanacademy/wonder-blocks-core";
3+
import {Id, View} from "@khanacademy/wonder-blocks-core";
54

65
import {
76
Unmounter,
@@ -30,7 +29,7 @@ export default {
3029
} as Meta;
3130

3231
export const IncorrectUsage = () => (
33-
<IDProvider id="incorrect" scope="example">
32+
<Id>
3433
{(id) => (
3534
<View>
3635
<Unmounter>
@@ -39,11 +38,11 @@ export const IncorrectUsage = () => (
3938
<View id={id} />
4039
</View>
4140
)}
42-
</IDProvider>
41+
</Id>
4342
);
4443

4544
export const CorrectUsage = () => (
46-
<IDProvider id="correct" scope="example">
45+
<Id>
4746
{(id) => (
4847
<View>
4948
<Unmounter>
@@ -52,5 +51,5 @@ export const CorrectUsage = () => (
5251
<View id={id} />
5352
</View>
5453
)}
55-
</IDProvider>
54+
</Id>
5655
);

packages/wonder-blocks-accordion/src/components/accordion-section.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import * as React from "react";
22
import {StyleSheet} from "aphrodite";
33
import type {StyleDeclaration} from "aphrodite";
44

5-
// eslint-disable-next-line import/no-deprecated
6-
import {useUniqueIdWithMock, View} from "@khanacademy/wonder-blocks-core";
5+
import {View} from "@khanacademy/wonder-blocks-core";
76
import * as tokens from "@khanacademy/wonder-blocks-tokens";
87
import {Body} from "@khanacademy/wonder-blocks-typography";
98
import type {AriaProps, StyleType} from "@khanacademy/wonder-blocks-core";
109

10+
import {useId} from "react";
1111
import type {AccordionCornerKindType} from "./accordion";
1212
import AccordionSectionHeader from "./accordion-section-header";
1313

@@ -204,15 +204,15 @@ const AccordionSection = React.forwardRef(function AccordionSection(
204204

205205
const controlledMode = expanded !== undefined && onToggle;
206206

207-
// eslint-disable-next-line import/no-deprecated
208-
const ids = useUniqueIdWithMock();
209-
const sectionId = id ?? ids.get("accordion-section");
207+
const uniqueSectionId = useId();
208+
const sectionId = id ?? uniqueSectionId;
210209
// We need an ID for the header so that the content section's
211210
// aria-labelledby attribute can point to it.
212-
const headerId = id ? `${id}-header` : ids.get("accordion-section-header");
211+
const uniqueHeaderId = useId();
212+
const headerId = id ? `${id}-header` : uniqueHeaderId;
213213
// We need an ID for the content section so that the opener's
214214
// aria-controls attribute can point to it.
215-
const sectionContentUniqueId = ids.get("accordion-section-content");
215+
const sectionContentUniqueId = useId();
216216

217217
const sectionStyles = _generateStyles(
218218
cornerKind,

packages/wonder-blocks-core/src/components/id-provider.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type Props = {
2828
};
2929

3030
/**
31+
* @deprecated This component is deprecated and will be removed in an
32+
* upcoming release. Migrate existing code to use `useId` or the `Id` component.
33+
*
3134
* This is a wrapper that returns an identifier. If the `id` prop is set, the
3235
* component will return the same id to be consumed by its children. Otherwise,
3336
* a unique id will be provided. This is beneficial for accessibility purposes,
@@ -54,9 +57,6 @@ type Props = {
5457
* )}
5558
* </IDProvider>
5659
* ```
57-
*
58-
* @deprecated Use `useId` for your ID needs. If you are in a class-based
59-
* component and cannot use hooks, then use the `Id` component.
6060
*/
6161
export default class IDProvider extends React.Component<Props> {
6262
static defaultId = "wb-id";

packages/wonder-blocks-core/src/components/id.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ type Props = {
1616
};
1717

1818
/**
19-
* A component that provides an identifier to its children.
19+
* `Id` is a component that provides an identifier to its children.
20+
*
21+
* It is useful for situations where the `useId` hook cannot be easily used,
22+
* such as in class-based components.
2023
*
2124
* If an `id` prop is provided, that is passed through to the children;
2225
* otherwise, a unique identifier is generated.

packages/wonder-blocks-core/src/components/unique-id-provider.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ type Props = {
4444
};
4545

4646
/**
47+
* @deprecated This component is deprecated and will be removed in an
48+
* upcoming release. Migrate existing code to use `useId` or the `Id` component.
49+
*
4750
* The `UniqueIDProvider` component is how Wonder Blocks components obtain
4851
* unique identifiers. This component ensures that server-side rendering and
4952
* initial client rendering match while allowing the provision of unique
@@ -70,9 +73,6 @@ type Props = {
7073
* )}
7174
* </UniqueIDProvider>
7275
* ```
73-
*
74-
* @deprecated Use `useId` for your ID needs. If you are in a class-based
75-
* component and cannot use hooks, then use the `Id` component.
7676
*/
7777
export default class UniqueIDProvider extends React.Component<Props> {
7878
// @ts-expect-error [FEI-5019] - TS2564 - Property '_idFactory' has no initializer and is not definitely assigned in the constructor.

packages/wonder-blocks-dropdown/src/components/__tests__/action-menu.test.tsx

+7-18
Original file line numberDiff line numberDiff line change
@@ -907,11 +907,8 @@ describe("ActionMenu", () => {
907907
const opener = await screen.findByRole("button");
908908

909909
// Assert
910-
// Expect autogenerated id to be in the form uid-action-menu-opener-[number]-wb-id
911-
expect(opener).toHaveAttribute(
912-
"id",
913-
expect.stringMatching(/^uid-action-menu-opener-\d+-wb-id$/),
914-
);
910+
// Expect autogenerated id
911+
expect(opener).toHaveAttribute("id", expect.any(String));
915912
});
916913

917914
it("Should use the `id` prop if provided", async () => {
@@ -929,6 +926,7 @@ describe("ActionMenu", () => {
929926
// Assert
930927
expect(opener).toHaveAttribute("id", id);
931928
});
929+
932930
it("Should auto-generate an id for the dropdown if `dropdownId` prop is not provided", async () => {
933931
// Arrange
934932
render(
@@ -945,10 +943,7 @@ describe("ActionMenu", () => {
945943
// Assert
946944
expect(
947945
await screen.findByRole("menu", {hidden: true}),
948-
).toHaveAttribute(
949-
"id",
950-
expect.stringMatching(/^uid-action-menu-dropdown-\d+-wb-id$/),
951-
);
946+
).toHaveAttribute("id", expect.any(String));
952947
});
953948

954949
it("Should use the `dropdownId` prop if provided", async () => {
@@ -1007,10 +1002,7 @@ describe("ActionMenu", () => {
10071002

10081003
// Assert
10091004
expect(opener).toHaveAttribute("aria-controls", dropdown.id);
1010-
expect(opener).toHaveAttribute(
1011-
"aria-controls",
1012-
expect.stringMatching(/^uid-action-menu-dropdown-\d+-wb-id$/),
1013-
);
1005+
expect(dropdown.id).toBeString();
10141006
});
10151007

10161008
it("Should set the `aria-controls` attribute on the custom opener to the provided dropdownId prop", async () => {
@@ -1035,7 +1027,7 @@ describe("ActionMenu", () => {
10351027

10361028
// Assert
10371029
expect(opener).toHaveAttribute("aria-controls", dropdown.id);
1038-
expect(opener).toHaveAttribute("aria-controls", dropdownId);
1030+
expect(dropdown.id).toBe(dropdownId);
10391031
});
10401032

10411033
it("Should set the `aria-controls` attribute on the custom opener to the auto-generated dropdownId", async () => {
@@ -1058,10 +1050,7 @@ describe("ActionMenu", () => {
10581050

10591051
// Assert
10601052
expect(opener).toHaveAttribute("aria-controls", dropdown.id);
1061-
expect(opener).toHaveAttribute(
1062-
"aria-controls",
1063-
expect.stringMatching(/^uid-action-menu-dropdown-\d+-wb-id$/),
1064-
);
1053+
expect(dropdown.id).toBeString();
10651054
});
10661055
});
10671056

packages/wonder-blocks-dropdown/src/components/__tests__/multi-select.test.tsx

+7-16
Original file line numberDiff line numberDiff line change
@@ -1828,11 +1828,9 @@ describe("MultiSelect", () => {
18281828
const opener = await screen.findByRole("button");
18291829

18301830
// Assert
1831-
expect(opener).toHaveAttribute(
1832-
"id",
1833-
expect.stringMatching(/^uid-multi-select-opener-\d+-wb-id$/),
1834-
);
1831+
expect(opener).toHaveAttribute("id", expect.any(String));
18351832
});
1833+
18361834
it("Should use the `id` prop if provided", async () => {
18371835
// Arrange
18381836
const id = "test-id";
@@ -1849,6 +1847,7 @@ describe("MultiSelect", () => {
18491847
// Assert
18501848
expect(opener).toHaveAttribute("id", id);
18511849
});
1850+
18521851
it("Should auto-generate an id for the dropdown if `dropdownId` prop is not provided", async () => {
18531852
// Arrange
18541853
const {userEvent} = doRender(
@@ -1866,11 +1865,9 @@ describe("MultiSelect", () => {
18661865
// Assert
18671866
expect(
18681867
await screen.findByRole("listbox", {hidden: true}),
1869-
).toHaveAttribute(
1870-
"id",
1871-
expect.stringMatching(/^uid-multi-select-dropdown-\d+-wb-id$/),
1872-
);
1868+
).toHaveAttribute("id", expect.any(String));
18731869
});
1870+
18741871
it("Should use the `dropdownId` prop if provided", async () => {
18751872
// Arrange
18761873
const dropdownId = "test-id";
@@ -1930,10 +1927,7 @@ describe("MultiSelect", () => {
19301927

19311928
// Assert
19321929
expect(opener).toHaveAttribute("aria-controls", dropdown.id);
1933-
expect(opener).toHaveAttribute(
1934-
"aria-controls",
1935-
expect.stringMatching(/^uid-multi-select-dropdown-\d+-wb-id$/),
1936-
);
1930+
expect(opener).toHaveAttribute("aria-controls", expect.any(String));
19371931
});
19381932

19391933
it("Should set the `aria-controls` attribute on the custom opener to the provided dropdownId prop", async () => {
@@ -1983,10 +1977,7 @@ describe("MultiSelect", () => {
19831977

19841978
// Assert
19851979
expect(opener).toHaveAttribute("aria-controls", dropdown.id);
1986-
expect(opener).toHaveAttribute(
1987-
"aria-controls",
1988-
expect.stringMatching(/^uid-multi-select-dropdown-\d+-wb-id$/),
1989-
);
1980+
expect(opener).toHaveAttribute("aria-controls", expect.any(String));
19901981
});
19911982
});
19921983

packages/wonder-blocks-dropdown/src/components/__tests__/single-select.test.tsx

+7-16
Original file line numberDiff line numberDiff line change
@@ -1396,11 +1396,9 @@ describe("SingleSelect", () => {
13961396
const opener = await screen.findByRole("button");
13971397

13981398
// Assert
1399-
expect(opener).toHaveAttribute(
1400-
"id",
1401-
expect.stringMatching(/^uid-single-select-opener-\d+-wb-id$/),
1402-
);
1399+
expect(opener).toHaveAttribute("id", expect.any(String));
14031400
});
1401+
14041402
it("Should use the `id` prop if provided", async () => {
14051403
// Arrange
14061404
const id = "test-id";
@@ -1417,6 +1415,7 @@ describe("SingleSelect", () => {
14171415
// Assert
14181416
expect(opener).toHaveAttribute("id", id);
14191417
});
1418+
14201419
it("Should auto-generate an id for the dropdown if `dropdownId` prop is not provided", async () => {
14211420
// Arrange
14221421
const {userEvent} = doRender(
@@ -1434,11 +1433,9 @@ describe("SingleSelect", () => {
14341433
// Assert
14351434
expect(
14361435
await screen.findByRole("listbox", {hidden: true}),
1437-
).toHaveAttribute(
1438-
"id",
1439-
expect.stringMatching(/^uid-single-select-dropdown-\d+-wb-id$/),
1440-
);
1436+
).toHaveAttribute("id", expect.any(String));
14411437
});
1438+
14421439
it("Should use the `dropdownId` prop if provided", async () => {
14431440
// Arrange
14441441
const dropdownId = "test-id";
@@ -1506,10 +1503,7 @@ describe("SingleSelect", () => {
15061503

15071504
// Assert
15081505
expect(opener).toHaveAttribute("aria-controls", dropdown.id);
1509-
expect(opener).toHaveAttribute(
1510-
"aria-controls",
1511-
expect.stringMatching(/^uid-single-select-dropdown-\d+-wb-id$/),
1512-
);
1506+
expect(opener).toHaveAttribute("aria-controls", expect.any(String));
15131507
});
15141508

15151509
it("Should set the `aria-controls` attribute on the custom opener to the provided dropdownId prop", async () => {
@@ -1561,10 +1555,7 @@ describe("SingleSelect", () => {
15611555

15621556
// Assert
15631557
expect(opener).toHaveAttribute("aria-controls", dropdown.id);
1564-
expect(opener).toHaveAttribute(
1565-
"aria-controls",
1566-
expect.stringMatching(/^uid-single-select-dropdown-\d+-wb-id$/),
1567-
);
1558+
expect(opener).toHaveAttribute("aria-controls", expect.any(String));
15681559
});
15691560
});
15701561

0 commit comments

Comments
 (0)