From 15f9747747851f42b985c139406c47ec98059b8c Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Mon, 8 Dec 2025 09:40:15 -0800 Subject: [PATCH 01/23] docs: Type check examples --- Makefile | 2 +- scripts/extractExamplesS2.mjs | 117 ++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 scripts/extractExamplesS2.mjs diff --git a/Makefile b/Makefile index 8ee3b3a4bb3..f6e4edb55b2 100644 --- a/Makefile +++ b/Makefile @@ -119,7 +119,7 @@ website-production: $(MAKE) s2-storybook-docs check-examples: - node scripts/extractExamples.mjs + node scripts/extractExamplesS2.mjs yarn tsc --project dist/docs-examples/tsconfig.json starter: diff --git a/scripts/extractExamplesS2.mjs b/scripts/extractExamplesS2.mjs new file mode 100644 index 00000000000..ea18d51a86b --- /dev/null +++ b/scripts/extractExamplesS2.mjs @@ -0,0 +1,117 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import fs from 'fs/promises'; +import json5 from 'json5'; +import path from 'path'; +import {transformAsync} from '@parcel/rust/lib/index.js'; + +let distDir = 'dist/docs-examples'; + +async function extractExamples() { + try { + await fs.rm(distDir, {recursive: true}); + } catch {} + await fs.mkdir(distDir, {recursive: true}); + + let pages = []; + for await (let page of fs.glob('packages/dev/s2-docs/pages/**/*.mdx')) { + pages.push(page); + } + + await Promise.all(pages.map(async page => { + let code = await fs.readFile(page, 'utf8'); + + code = code.replace(/()((?:.|\n)*?)<\/ExampleSwitcher>/g, (m, start, examples, inner) => { + if (inner.includes('COMPONENT')) { + let components = json5.parse(examples); + inner = components.map(component => inner.replace(/COMPONENT/g, component)).join('\n\n'); + return start + inner + ''; + } + + return m; + }); + + let res = await transformAsync({ + filename: page, + code: Buffer.from(code), + module_id: '123', + project_root: process.cwd(), + inline_fs: false, + env: {}, + type: 'mdx', + context: 'react-server', + automatic_jsx_runtime: true, + decorators: false, + use_define_for_class_fields: false, + is_development: false, + react_refresh: false, + source_maps: false, + scope_hoist: false, + source_type: 'Module', + supports_module_workers: true, + is_library: false, + is_esm_output: false, + trace_bailouts: false, + is_swc_helpers: false, + standalone: false, + inline_constants: false + }); + + for (let asset of res.mdx_assets) { + if (asset.lang === 'tsx' || asset.lang === 'ts') { + let relative = path.relative(path.join(process.cwd(), 'packages/dev/s2-docs/pages'), page); + await fs.mkdir(path.join(distDir, path.dirname(relative)), {recursive: true}); + let code = `// ${page}:${asset.position.start.line}:${asset.position.start.column}\n\n${asset.code}`; + await fs.writeFile(path.join(distDir, relative + ':' + asset.position.start.line + '.' + asset.lang), code); + } + } + })); + + for await (let file of fs.glob('packages/dev/s2-docs/pages/**/*.{tsx,ts,json}')) { + let relative = path.relative(path.join(process.cwd(), 'packages/dev/s2-docs/pages'), file); + await fs.mkdir(path.join(distDir, path.dirname(relative)), {recursive: true}); + await fs.copyFile(file, path.join(distDir, relative)); + } + + await fs.copyFile('lib/svg.d.ts', `${distDir}/svg.d.ts`); + await fs.copyFile('lib/css.d.ts', `${distDir}/css.d.ts`); + await fs.writeFile(`${distDir}/tsconfig.json`, `{ + "compilerOptions": { + "target": "es2018", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "strict": true, + "noImplicitAny": false, + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "noUnusedLocals": true, + "paths": { + "vanilla-starter/*": ["../../starters/docs/src/*"], + "tailwind-starter/*": ["../../starters/tailwind/src/*"] + } + } + } + `); +} + +extractExamples(); From 41175b9dd8ce3a3bfbcb4c1282f24440c37ec3e3 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 8 Dec 2025 12:47:03 -0600 Subject: [PATCH 02/23] fix some type errors --- .../s2-docs/pages/react-aria/DropTarget.tsx | 2 +- .../s2-docs/pages/react-aria/PokemonTree.tsx | 2 +- .../pages/react-aria/blog/CalendarSystems.tsx | 4 +- .../blog/DragBetweenListsExample.tsx | 21 +++++++--- .../dev/s2-docs/pages/s2/ActionButton.mdx | 1 - .../dev/s2-docs/pages/s2/CheckboxGroup.mdx | 2 +- packages/dev/s2-docs/pages/s2/ColorField.mdx | 8 ++-- packages/dev/s2-docs/pages/s2/ComboBox.mdx | 16 ++++--- packages/dev/s2-docs/pages/s2/DateField.mdx | 4 +- packages/dev/s2-docs/pages/s2/DatePicker.mdx | 4 +- .../dev/s2-docs/pages/s2/DateRangePicker.mdx | 9 ++-- packages/dev/s2-docs/pages/s2/DropZone.mdx | 5 ++- packages/dev/s2-docs/pages/s2/Form.mdx | 5 ++- packages/dev/s2-docs/pages/s2/RadioGroup.mdx | 2 +- .../dev/s2-docs/pages/s2/SegmentedControl.mdx | 4 +- packages/dev/s2-docs/pages/s2/TagGroup.mdx | 4 +- packages/dev/s2-docs/pages/s2/TimeField.mdx | 2 +- packages/dev/s2-docs/pages/s2/TreeView.mdx | 42 ++++++++++--------- packages/dev/s2-docs/pages/s2/forms.mdx | 5 ++- starters/docs/src/Calendar.tsx | 3 +- starters/docs/src/ColorField.tsx | 1 - starters/docs/src/Menu.tsx | 1 - starters/tailwind/src/Popover.tsx | 4 +- starters/tailwind/src/Tree.tsx | 1 - 24 files changed, 82 insertions(+), 70 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx b/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx index 826392568d0..dc6c5a2ba6d 100644 --- a/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx +++ b/packages/dev/s2-docs/pages/react-aria/DropTarget.tsx @@ -1,6 +1,6 @@ "use client"; -import React, {JSX, ReactNode} from 'react'; +import React, {ReactNode} from 'react'; import type {TextDropItem} from '@react-aria/dnd'; import {useDrop} from '@react-aria/dnd'; diff --git a/packages/dev/s2-docs/pages/react-aria/PokemonTree.tsx b/packages/dev/s2-docs/pages/react-aria/PokemonTree.tsx index 74c18d21b8c..b7ff33c76dd 100644 --- a/packages/dev/s2-docs/pages/react-aria/PokemonTree.tsx +++ b/packages/dev/s2-docs/pages/react-aria/PokemonTree.tsx @@ -52,7 +52,7 @@ export function PokemonTree(props: PokemonTreeProps) { dragAndDropHooks={dragAndDropHooks}> {function renderItem(item: Pokemon) { return ( - {item.name}{item.type}}> + {item.name}{item.type}} textValue={item.name}> {renderItem} diff --git a/packages/dev/s2-docs/pages/react-aria/blog/CalendarSystems.tsx b/packages/dev/s2-docs/pages/react-aria/blog/CalendarSystems.tsx index 6eca85701e8..6783d90e2da 100644 --- a/packages/dev/s2-docs/pages/react-aria/blog/CalendarSystems.tsx +++ b/packages/dev/s2-docs/pages/react-aria/blog/CalendarSystems.tsx @@ -1,12 +1,12 @@ 'use client'; -import {Calendar, Picker, PickerItem, Provider} from '@react-spectrum/s2'; +import {Calendar, Picker, PickerItem, Provider, type Key} from '@react-spectrum/s2'; import React from 'react'; import {useLocale} from '@react-aria/i18n'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; export default function CalendarSystems() { - let [calendar, setCalendar] = React.useState('gregory'); + let [calendar, setCalendar] = React.useState('gregory'); let {locale} = useLocale(); const calendars = [ {key: 'gregory', name: 'Gregorian'}, diff --git a/packages/dev/s2-docs/pages/react-aria/blog/DragBetweenListsExample.tsx b/packages/dev/s2-docs/pages/react-aria/blog/DragBetweenListsExample.tsx index cef69a845c7..d61a1e5b6ce 100644 --- a/packages/dev/s2-docs/pages/react-aria/blog/DragBetweenListsExample.tsx +++ b/packages/dev/s2-docs/pages/react-aria/blog/DragBetweenListsExample.tsx @@ -2,10 +2,21 @@ import {ListBox, ListBoxItem} from 'vanilla-starter/ListBox'; import {Folder, File} from 'lucide-react'; -import {useDragAndDrop, isTextDropItem, useListData} from 'react-aria-components'; +import {useDragAndDrop, isTextDropItem, useListData, ListData} from 'react-aria-components'; import React from 'react'; -function BidirectionalDnDListBox(props) { +interface ListItem { + id: string; + type: 'file' | 'folder'; + name: string; +} + +interface BidirectionalDnDListBoxProps { + list: ListData; + 'aria-label': string; +} + +function BidirectionalDnDListBox(props: BidirectionalDnDListBoxProps) { let {list} = props; let {dragAndDropHooks} = useDragAndDrop({ acceptedDragTypes: ['custom-app-type-bidirectional'], @@ -13,7 +24,7 @@ function BidirectionalDnDListBox(props) { getAllowedDropOperations: () => ['move'], getItems(keys) { return [...keys].map(key => { - let item = list.getItem(key); + let item = list.getItem(key)!; // Setup the drag types and associated info for each dragged item. return { 'custom-app-type-bidirectional': JSON.stringify(item), @@ -93,7 +104,7 @@ function BidirectionalDnDListBox(props) { } export default function DragBetweenListsExample() { - let list1 = useListData({ + let list1 = useListData({ initialItems: [ {id: '1', type: 'file', name: 'Adobe Photoshop'}, {id: '2', type: 'file', name: 'Adobe XD'}, @@ -104,7 +115,7 @@ export default function DragBetweenListsExample() { ] }); - let list2 = useListData({ + let list2 = useListData({ initialItems: [ {id: '7', type: 'folder', name: 'Pictures'}, {id: '8', type: 'file', name: 'Adobe Fresco'}, diff --git a/packages/dev/s2-docs/pages/s2/ActionButton.mdx b/packages/dev/s2-docs/pages/s2/ActionButton.mdx index fd8d70719c6..4767a7180a9 100644 --- a/packages/dev/s2-docs/pages/s2/ActionButton.mdx +++ b/packages/dev/s2-docs/pages/s2/ActionButton.mdx @@ -56,7 +56,6 @@ function PendingButton() { return ( Cookie policy {/*- end highlight -*/} - + ``` diff --git a/packages/dev/s2-docs/pages/s2/ColorField.mdx b/packages/dev/s2-docs/pages/s2/ColorField.mdx index 498e12ad3bf..c1fccda1487 100644 --- a/packages/dev/s2-docs/pages/s2/ColorField.mdx +++ b/packages/dev/s2-docs/pages/s2/ColorField.mdx @@ -26,13 +26,13 @@ Use the `value` or `defaultValue` prop to set the color value, and `onChange` to ```tsx render "use client"; -import {ColorField} from '@react-spectrum/s2'; +import {ColorField, type Color} from '@react-spectrum/s2'; import {useState} from 'react'; import {parseColor} from '@react-stately/color'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; function Example() { - let [value, setValue] = useState(parseColor('#e73623')); + let [value, setValue] = useState(parseColor('#e73623')); return (
@@ -53,12 +53,12 @@ By default, ColorField displays a hex value. Set the `colorSpace` and `channel` ```tsx render "use client"; -import {ColorField, parseColor} from '@react-spectrum/s2'; +import {ColorField, parseColor, type Color} from '@react-spectrum/s2'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {useState} from 'react'; function Example() { - let [color, setColor] = useState(parseColor('#7f007f')); + let [color, setColor] = useState(parseColor('#7f007f')); return (
diff --git a/packages/dev/s2-docs/pages/s2/ComboBox.mdx b/packages/dev/s2-docs/pages/s2/ComboBox.mdx index ae048b33a25..971708c39a0 100644 --- a/packages/dev/s2-docs/pages/s2/ComboBox.mdx +++ b/packages/dev/s2-docs/pages/s2/ComboBox.mdx @@ -49,7 +49,7 @@ function Example() { return ( /*- begin highlight -*/ - + {(item) => {item.name}} /*- end highlight -*/ @@ -158,8 +158,12 @@ Use the `loadingState` and `onLoadMore` props to enable async loading and infini "use client"; import {ComboBox, ComboBoxItem, useAsyncList} from '@react-spectrum/s2'; +interface Character { + name: string +} + function Example() { - let list = useAsyncList({ + let list = useAsyncList({ async load({signal, cursor, filterText}) { if (cursor) { cursor = cursor.replace(/^http:\/\//i, 'https://'); @@ -254,7 +258,7 @@ import {ComboBox, ComboBoxItem, type Key} from '@react-spectrum/s2'; import {useState} from 'react'; function Example() { - let [animal, setAnimal] = useState("bison"); + let [animal, setAnimal] = useState("bison"); return (
@@ -339,15 +343,15 @@ function ControlledComboBox() { ]; /*- end collapse -*/ - let [fieldState, setFieldState] = useState({ + let [fieldState, setFieldState] = useState<{selectedKey: Key | null, inputValue: string}>({ selectedKey: null, inputValue: '' }); - let onSelectionChange = (id: Key) => { + let onSelectionChange = (id: Key | null) => { // Update inputValue when selectedKey changes. setFieldState({ - inputValue: options.find(o => o.id === id)?.name ?? '', + inputValue: id ? (options.find(o => o.id === id)?.name ?? '') : '', selectedKey: id }); }; diff --git a/packages/dev/s2-docs/pages/s2/DateField.mdx b/packages/dev/s2-docs/pages/s2/DateField.mdx index 9a966c8362f..42568881460 100644 --- a/packages/dev/s2-docs/pages/s2/DateField.mdx +++ b/packages/dev/s2-docs/pages/s2/DateField.mdx @@ -25,13 +25,13 @@ Use the `value` or `defaultValue` prop to set the date value, using objects in t ```tsx render "use client"; -import {parseDate, getLocalTimeZone} from '@internationalized/date'; +import {parseDate, getLocalTimeZone, CalendarDate} from '@internationalized/date'; import {useDateFormatter} from 'react-aria'; import {DateField} from '@react-spectrum/s2'; import {useState} from 'react'; function Example() { - let [date, setDate] = useState(parseDate('2020-02-03')); + let [date, setDate] = useState(parseDate('2020-02-03')); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( diff --git a/packages/dev/s2-docs/pages/s2/DatePicker.mdx b/packages/dev/s2-docs/pages/s2/DatePicker.mdx index 747569f160a..a4f06c9e831 100644 --- a/packages/dev/s2-docs/pages/s2/DatePicker.mdx +++ b/packages/dev/s2-docs/pages/s2/DatePicker.mdx @@ -25,13 +25,13 @@ Use the `value` or `defaultValue` prop to set the date value, using objects in t ```tsx render "use client"; -import {parseDate, getLocalTimeZone} from '@internationalized/date'; +import {parseDate, getLocalTimeZone, CalendarDate} from '@internationalized/date'; import {useDateFormatter} from 'react-aria'; import {DatePicker} from '@react-spectrum/s2'; import {useState} from 'react'; function Example() { - let [date, setDate] = useState(parseDate('2020-02-03')); + let [date, setDate] = useState(parseDate('2020-02-03')); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( diff --git a/packages/dev/s2-docs/pages/s2/DateRangePicker.mdx b/packages/dev/s2-docs/pages/s2/DateRangePicker.mdx index 72966a7652e..c9872b68fae 100644 --- a/packages/dev/s2-docs/pages/s2/DateRangePicker.mdx +++ b/packages/dev/s2-docs/pages/s2/DateRangePicker.mdx @@ -31,13 +31,14 @@ Use the `value` or `defaultValue` prop to set the selected date range, using obj ```tsx render "use client"; -import {parseDate, getLocalTimeZone} from '@internationalized/date'; +import {parseDate, getLocalTimeZone, CalendarDate} from '@internationalized/date'; import {useDateFormatter} from 'react-aria'; import {DateRangePicker} from '@react-spectrum/s2'; +import type {RangeValue} from '@react-types/shared'; import {useState} from 'react'; function Example() { - let [range, setRange] = useState({ + let [range, setRange] = useState | null>({ start: parseDate('2025-02-03'), end: parseDate('2025-02-12') }); @@ -50,10 +51,10 @@ function Example() { value={range} onChange={setRange} /> {/*- end highlight -*/} -

Selected range: {formatter.formatRange( +

Selected range: {range ? formatter.formatRange( range.start.toDate(getLocalTimeZone()), range.end.toDate(getLocalTimeZone()) - )}

+ ) : '--'}

); } diff --git a/packages/dev/s2-docs/pages/s2/DropZone.mdx b/packages/dev/s2-docs/pages/s2/DropZone.mdx index 04d5f469f18..ac30dfa1a56 100644 --- a/packages/dev/s2-docs/pages/s2/DropZone.mdx +++ b/packages/dev/s2-docs/pages/s2/DropZone.mdx @@ -13,12 +13,12 @@ export const description = 'An area into which one or multiple objects can be dr ```tsx render type="s2" docs={docs.exports.DropZone} links={docs.links} props={['size', 'replaceMessage']} wide "use client"; import {DropZone, IllustratedMessage, Heading, Content, ButtonGroup, Button, FileTrigger} from '@react-spectrum/s2'; -import {useState} from 'react'; +import React, {useState} from 'react'; import CloudUpload from '@react-spectrum/s2/illustrations/gradient/generic1/CloudUpload'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; function Example(props) { - let [content, setContent] = useState(null); + let [content, setContent] = useState(null); return ( { + if (!files) return; let url = URL.createObjectURL(files[0]); setContent({files[0].name}); }}> diff --git a/packages/dev/s2-docs/pages/s2/Form.mdx b/packages/dev/s2-docs/pages/s2/Form.mdx index 5aaa30ef302..221f367d9b8 100644 --- a/packages/dev/s2-docs/pages/s2/Form.mdx +++ b/packages/dev/s2-docs/pages/s2/Form.mdx @@ -59,12 +59,13 @@ import {Form, TextField, Button} from '@react-spectrum/s2'; event.preventDefault(); // Get data from form. - let formData = new FormData(event.target); + let target = event.target as HTMLFormElement; + let formData = new FormData(target); let name = formData.get('name'); alert(`Hello, ${name}!`); // Reset form after submission. - event.target.reset(); + target.reset(); }}> {/*- end highlight -*/} diff --git a/packages/dev/s2-docs/pages/s2/RadioGroup.mdx b/packages/dev/s2-docs/pages/s2/RadioGroup.mdx index e219a9dbbc6..70fc70de365 100644 --- a/packages/dev/s2-docs/pages/s2/RadioGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/RadioGroup.mdx @@ -34,7 +34,7 @@ import {RadioGroup, Radio} from '@react-spectrum/s2'; import {useState} from 'react'; function Example() { - let [selected, setSelected] = useState(null); + let [selected, setSelected] = useState(null); return ( <> diff --git a/packages/dev/s2-docs/pages/s2/SegmentedControl.mdx b/packages/dev/s2-docs/pages/s2/SegmentedControl.mdx index cc41db01912..f0071e2feff 100644 --- a/packages/dev/s2-docs/pages/s2/SegmentedControl.mdx +++ b/packages/dev/s2-docs/pages/s2/SegmentedControl.mdx @@ -83,11 +83,11 @@ Use the `defaultSelectedKey` or `selectedKey` prop to set the selected item. The ```tsx render "use client"; -import {SegmentedControl, SegmentedControlItem} from '@react-spectrum/s2'; +import {SegmentedControl, SegmentedControlItem, type Key} from '@react-spectrum/s2'; import {useState} from 'react'; function Example() { - let [selected, setSelected] = useState('month'); + let [selected, setSelected] = useState('month'); return (
diff --git a/packages/dev/s2-docs/pages/s2/TagGroup.mdx b/packages/dev/s2-docs/pages/s2/TagGroup.mdx index 318e460c49d..0cc737a2820 100644 --- a/packages/dev/s2-docs/pages/s2/TagGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/TagGroup.mdx @@ -12,7 +12,6 @@ export const description = 'Displays a list of items, with support for keyboard ```tsx render docs={docs.exports.TagGroup} links={docs.links} props={['label', 'selectionMode', 'size', 'labelPosition', 'maxRows', 'description', 'contextualHelp', 'isEmphasized']} initialProps={{label: 'Ice cream flavors', selectionMode: 'multiple', maxRows: 2}} controlOptions={{maxRows: {minValue: 1}}} type="s2" import {TagGroup, Tag} from '@react-spectrum/s2'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; Chocolate @@ -204,9 +203,8 @@ Use the `selectionMode` prop to enable single or multiple selection. The selecte ```tsx render docs={docs.exports.TagGroup} links={docs.links} props={['selectionMode', 'selectionBehavior', 'disallowEmptySelection']} initialProps={{selectionMode: 'multiple'}} wide "use client"; -import {TagGroup, Tag} from '@react-spectrum/s2'; +import {TagGroup, Tag, type Selection} from '@react-spectrum/s2'; import {useState} from 'react'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; function Example(props) { let [selected, setSelected] = useState(new Set()); diff --git a/packages/dev/s2-docs/pages/s2/TimeField.mdx b/packages/dev/s2-docs/pages/s2/TimeField.mdx index 9bad9790770..5a717244559 100644 --- a/packages/dev/s2-docs/pages/s2/TimeField.mdx +++ b/packages/dev/s2-docs/pages/s2/TimeField.mdx @@ -31,7 +31,7 @@ import {TimeField} from '@react-spectrum/s2'; import {useState} from 'react'; function Example() { - let [time, setTime] = useState(new Time(11, 45)); + let [time, setTime] = useState