diff --git a/.github/workflows/weekly-api-diff.yml b/.github/workflows/weekly-api-diff.yml index 3a335397656..f6788ae04fc 100644 --- a/.github/workflows/weekly-api-diff.yml +++ b/.github/workflows/weekly-api-diff.yml @@ -27,7 +27,7 @@ jobs: - name: Build current API snapshot run: yarn build:api-branch - # Build release baseline using the last Publish commit (~2 min, always fresh) + # Build release baseline using the last minor/major release commit (skips patch-only releases) - name: Build release baseline run: yarn build:api-published @@ -50,12 +50,33 @@ jobs: CURRENT_PUBLISH=$(git log --grep='^Publish$' --oneline -1 | awk '{print $1}') PREV_PUBLISH=$(cat snapshots/last-publish-hash.txt 2>/dev/null || echo "") + echo "=== Release detection ===" + echo "CURRENT_PUBLISH=$CURRENT_PUBLISH" + echo "PREV_PUBLISH=$PREV_PUBLISH" + if [ -n "$PREV_PUBLISH" ] && [ "$CURRENT_PUBLISH" != "$PREV_PUBLISH" ]; then - # New release landed so skip comparing to last week, commit fresh as new baseline - echo "NEW_RELEASE=true" >> $GITHUB_ENV + # Detect minor/major vs patch: check if s2 or react-aria-components got a x.y.0 tag on this Publish commit + IS_MINOR=$(git tag --points-at "$CURRENT_PUBLISH" | grep -E '^(@react-spectrum/s2|react-aria-components)@[0-9]+\.[0-9]+\.0$' | head -1) + echo "IS_MINOR_OR_MAJOR=${IS_MINOR:-(none)}" + if [ -n "$IS_MINOR" ]; then + echo "Minor/major release detected, resetting baseline" + echo "NEW_RELEASE=true" >> $GITHUB_ENV + else + echo "Patch release detected, updating hash, continuing with delta" + echo "$CURRENT_PUBLISH" > snapshots/last-publish-hash.txt + fi else + echo "No new release" + fi + + NEW_RELEASE="${NEW_RELEASE:-false}" + + if [ "$NEW_RELEASE" != "true" ]; then # Compare against the last diff in the snapshots repo PREV=$(ls snapshots/diffs/*.txt 2>/dev/null | sort -r | head -1) + echo "" + echo "=== Delta computation ===" + echo "Comparing against: ${PREV:-(none, first run)}" if [ -n "$PREV" ]; then diff "$PREV" /tmp/diff-current.txt > /tmp/weekly-delta.txt || true else @@ -63,18 +84,48 @@ jobs: fi fi - # Commit a diff if there is a new release (fresh baseline), or diff changed from last week + echo "" + echo "=== File sizes ===" + echo "diff-current.txt: $(wc -c < /tmp/diff-current.txt) bytes" + ls /tmp/weekly-delta.txt 2>/dev/null && echo "weekly-delta.txt: $(wc -c < /tmp/weekly-delta.txt) bytes" || echo "weekly-delta.txt: not created" + echo "" + echo "=== Delta content (first 20 lines) ===" + head -20 /tmp/weekly-delta.txt 2>/dev/null || echo "(none)" + + # Commit a diff if there is a new minor/major release (fresh baseline), or diff changed from last week # Skip if no difference from last diff (aka no change from last week/last run), or if the diff against the release code is empty - NEW_RELEASE="${NEW_RELEASE:-false}" + echo "" + echo "=== Commit decision ===" if [ -s /tmp/diff-current.txt ] && ([ "$NEW_RELEASE" = "true" ] || [ -s /tmp/weekly-delta.txt ]); then + echo "Committing diff to snapshots repo" cp /tmp/diff-current.txt snapshots/diffs/$TODAY.txt + mkdir -p snapshots/deltas + if [ -s /tmp/weekly-delta.txt ]; then + # Save processed delta: strip > / < markers and line-number markers, + # split into two labeled sections so it can be passed directly to the model + { + echo "=== NEW API CHANGES THIS WEEK (not in last week's diff) ===" + grep '^> ' /tmp/weekly-delta.txt | sed 's/^> //' + RELEASED=$(grep '^< ' /tmp/weekly-delta.txt | sed 's/^< //') + if [ -n "$RELEASED" ]; then + echo "" + echo "=== API CHANGES RELEASED SINCE LAST WEEK (were in last week's diff, now gone) ===" + echo "$RELEASED" + fi + } > snapshots/deltas/$TODAY.txt + fi cd snapshots git config user.email "github-actions@github.com" git config user.name "GitHub Actions" git add diffs/$TODAY.txt + [ -f deltas/$TODAY.txt ] && git add deltas/$TODAY.txt echo "$CURRENT_PUBLISH" > last-publish-hash.txt git add last-publish-hash.txt + echo "=== Staged changes ===" + git diff --cached --stat git diff --cached --quiet || (git commit -m "weekly api diff $TODAY" && git push) + else + echo "Skipping commit — diff-current empty=$([ ! -s /tmp/diff-current.txt ] && echo yes || echo no), new_release=$NEW_RELEASE, delta_empty=$([ ! -s /tmp/weekly-delta.txt ] && echo yes || echo no)" fi # Summarize with GitHub Models (free via GITHUB_TOKEN) and post to Slack @@ -82,6 +133,7 @@ jobs: env: SLACK_TSDIFF_CHROMATIC_BOT_TOKEN: ${{ secrets.SLACK_TSDIFF_CHROMATIC_BOT_TOKEN }} SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }} + TEST_SLACK_ID: ${{ secrets.TEST_SLACK_ID }} GITHUB_TOKEN: ${{ github.token }} run: | python3 << 'PYEOF' @@ -99,6 +151,7 @@ jobs: github_token = os.environ['GITHUB_TOKEN'] workspace = os.environ['GITHUB_WORKSPACE'] diff_url = f"https://github.com/{snapshots_repo}/blob/main/diffs/{today}.txt" + delta_url = f"https://github.com/{snapshots_repo}/blob/main/deltas/{today}.txt" vs_release_size = os.path.getsize('/tmp/diff-current.txt') vs_last_week_size = os.path.getsize('/tmp/weekly-delta.txt') if os.path.exists('/tmp/weekly-delta.txt') else 0 @@ -117,7 +170,9 @@ jobs: elif new_release: message = f"📊 Weekly API Diff — {today}\n\nNew release since last diff — resetting baseline. Full diff vs release: {diff_url}\n\nReact ✅ if changes look expected, or 🚨 if something looks wrong." else: - delta = open('/tmp/weekly-delta.txt').read()[:4000] + # Read the already-processed delta saved by the shell step + delta_path = f"{workspace}/snapshots/deltas/{today}.txt" + model_input = open(delta_path).read() if os.path.exists(delta_path) else "" # Extract classification rules from prompt.md (single source of truth) prompt_md = open(f"{workspace}/scripts/weekly-api-diff/prompt.md").read() @@ -127,10 +182,19 @@ jobs: payload = { "model": "gpt-4o-mini", - "max_tokens": 600, + "max_tokens": 800, "messages": [{ "role": "user", - "content": f"Summarize this week-to-week react-spectrum API diff in under 200 words using bullet points.\n\n{rules}\n\nDelta (changes from last week):\n{delta}" + "content": ( + f"Summarize this week's react-spectrum API changes using grouped bullet points. Group related changes together (e.g. multiple layout classes losing the same method = one bullet). For each group or item:\n" + f"- Name the component(s) or interface(s)\n" + f"- Say what changed (added, removed, signature changed)\n" + f"- Flag net-new components \n" + f"Don't spell out full type signatures. Aim for a knowledgeable teammate skimming Slack.\n\n" + f"IMPORTANT: Only report what is explicitly listed below. Do not infer or add anything from your training knowledge.\n\n" + f"{rules}\n\n" + f"{model_input}" + ) }] } @@ -143,7 +207,7 @@ jobs: } ) summary = json.loads(urllib.request.urlopen(req).read())['choices'][0]['message']['content'] - message = f"📊 Weekly API Diff — {today}\n\n{summary}\n\nFull diff vs release: {diff_url}\n\nReact ✅ if changes look expected, or 🚨 if something looks wrong." + message = f"📊 Weekly API Diff — {today}\n\n{summary}\n\nWhat's new this week: {delta_url}\nFull diff vs release: {diff_url}\n\nReact ✅ if changes look expected, or 🚨 if something looks wrong." req = urllib.request.Request( 'https://slack.com/api/chat.postMessage', diff --git a/.vscode/settings.json b/.vscode/settings.json index 3441d7ee320..0126f496d52 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "editor.defaultFormatter": "oxc.oxc-vscode", "editor.formatOnSave": true, - "editor.formatOnSaveMode": "file" + "editor.formatOnSaveMode": "file", + "typescript.experimental.useTsgo": true } diff --git a/package.json b/package.json index fef525dcb2e..29c841fb4f1 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "release": "lerna publish from-package --yes", "version:nightly": "yarn workspaces foreach --all --no-private -t version -d 3.0.0-nightly-$(git rev-parse --short HEAD)-$(date +'%y%m%d') && yarn apply-nightly --all", "publish:nightly": "yarn workspaces foreach --all --no-private -t npm publish --tag nightly --access public", - "build:api-published": "node scripts/buildBranchAPI.js --githash=$(git log --grep='^Publish$' --oneline -1 | awk '{print $1}') --output=base-api", + "build:api-published": "node scripts/buildBranchAPI.js --githash=$(git rev-list -n 1 $(git tag -l 'react-aria-components@*' | grep -E '@[0-9]+\\.[0-9]+\\.0$' | sort -V | tail -1)) --output=base-api", "build:api-branch": "node scripts/buildBranchAPI.js", "compare:apis": "node scripts/compareAPIs.js", "check-apis": "yarn build:api-branch --githash=\"origin/main\" --output=\"base-api\" && yarn build:api-branch && yarn compare:apis", diff --git a/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx b/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx index 2e1ca4d9a09..2cae147dade 100644 --- a/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ColorField.stories.tsx @@ -11,6 +11,7 @@ */ import {ColorField, ColorFieldProps} from '../src/ColorField'; +import {ColorSwatch} from '../exports'; import {generateComboChunks, shortName} from './utils'; import type {Meta, StoryObj} from '@storybook/react'; import {ReactElement} from 'react'; @@ -144,3 +145,11 @@ export const LabelPositionTopPt5: StoryObj = { }; // Skipped the contextual help stories from here on out since its all shared + +export const ExampleWithPrefix: StoryObj = { + render: args => , + args: { + label: 'Color', + prefix: + } +}; diff --git a/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx b/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx index 19ce8eb4ad9..50da3be60b4 100644 --- a/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/Combobox.stories.tsx @@ -22,6 +22,7 @@ import { WithAvatars, WithIcons } from '../stories/ComboBox.stories'; +import {Avatar} from '../exports'; import {ComboBox} from '../src/ComboBox'; import {expect} from '@storybook/jest'; import type {Meta, StoryObj} from '@storybook/react'; @@ -192,3 +193,10 @@ export const Filtering: StoryObj = { ); } }; + +export const WithPrefix: Story = { + ...Example, + args: { + prefix: + } +}; diff --git a/packages/@react-spectrum/s2/chromatic/NumberField.stories.tsx b/packages/@react-spectrum/s2/chromatic/NumberField.stories.tsx index c109055df57..61378db7142 100644 --- a/packages/@react-spectrum/s2/chromatic/NumberField.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/NumberField.stories.tsx @@ -111,3 +111,13 @@ export const ContextualHelpExample: Story = { ) } }; + +export const ExampleWithPrefix: Story = { + render: args => , + args: { + label: 'Price', + prefix: 'USD', + formatOptions: {style: 'currency', currency: 'USD', currencyDisplay: 'narrowSymbol'}, + placeholder: '0.00' + } +}; diff --git a/packages/@react-spectrum/s2/src/CenterBaseline.tsx b/packages/@react-spectrum/s2/src/CenterBaseline.tsx index 1b0bb224b02..84d941ef610 100644 --- a/packages/@react-spectrum/s2/src/CenterBaseline.tsx +++ b/packages/@react-spectrum/s2/src/CenterBaseline.tsx @@ -12,11 +12,13 @@ import {css} from '../style/style-macro' with {type: 'macro'}; import {CSSProperties, ReactNode} from 'react'; +import {DOMAttributes} from '@react-types/shared'; +import {filterDOMProps} from 'react-aria/filterDOMProps'; import {mergeStyles} from '../style/runtime'; import {style} from '../style' with {type: 'macro'}; import {StyleString} from '../style/types'; -interface CenterBaselineProps { +interface CenterBaselineProps extends DOMAttributes { style?: CSSProperties; styles?: StyleString; children: ReactNode; @@ -29,8 +31,10 @@ const styles = style({ }); export function CenterBaseline(props: CenterBaselineProps): ReactNode { + let domProps = filterDOMProps(props); return (
diff --git a/packages/@react-spectrum/s2/src/ColorField.tsx b/packages/@react-spectrum/s2/src/ColorField.tsx index e4b6d0548a8..e819c353c40 100644 --- a/packages/@react-spectrum/s2/src/ColorField.tsx +++ b/packages/@react-spectrum/s2/src/ColorField.tsx @@ -14,15 +14,23 @@ import { ColorField as AriaColorField, ColorFieldProps as AriaColorFieldProps } from 'react-aria-components/ColorField'; - import {ContextValue} from 'react-aria-components/slots'; -import {createContext, forwardRef, Ref, useContext, useImperativeHandle, useRef} from 'react'; +import { + createContext, + forwardRef, + ReactNode, + Ref, + useContext, + useImperativeHandle, + useRef +} from 'react'; import {createFocusableRef} from './useDOMRef'; import {field, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText, Input} from './Field'; import {FormContext, useFormProps} from './Form'; import {GlobalDOMAttributes, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; -import {InputProps} from 'react-aria-components/Input'; +import {InputContext, InputProps} from 'react-aria-components/Input'; +import {mergeRefs} from 'react-aria/mergeRefs'; import {style} from '../style' with {type: 'macro'}; import {TextFieldRef} from './TextField'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -43,6 +51,11 @@ export interface ColorFieldProps * @default 'M' */ size?: 'S' | 'M' | 'L' | 'XL'; + /** + * The prefix to display in the ColorField. A non-interactive element that appears before the + * input. + */ + prefix?: ReactNode; } export const ColorFieldContext = @@ -114,8 +127,18 @@ export const ColorField = forwardRef(function ColorField( contextualHelp={props.contextualHelp}> {label} - - + + + {ctx => ( + + + + )} + {isInvalid && } extends @@ -702,6 +706,7 @@ const ComboboxInner = forwardRef(function ComboboxInner( {label} {ctx => ( - + )} diff --git a/packages/@react-spectrum/s2/src/Field.tsx b/packages/@react-spectrum/s2/src/Field.tsx index 128d121cc2d..992810ecdc8 100644 --- a/packages/@react-spectrum/s2/src/Field.tsx +++ b/packages/@react-spectrum/s2/src/Field.tsx @@ -26,15 +26,19 @@ import { UnsafeStyles } from './style-utils' with {type: 'macro'}; import {FieldError, FieldErrorProps} from 'react-aria-components/FieldError'; -import {ForwardedRef, forwardRef, ReactNode} from 'react'; +import {ForwardedRef, forwardRef, ReactNode, useContext} from 'react'; import {getEventTarget} from 'react-aria/private/utils/shadowdom/DOMFunctions'; import {Group, GroupProps} from 'react-aria-components/Group'; import {IconContext} from './Icon'; +import { + InputContext, + Input as RACInput, + InputProps as RACInputProps +} from 'react-aria-components/Input'; import intlMessages from '../intl/*.json'; import {Label, LabelProps} from 'react-aria-components/Label'; import {mergeStyles} from '../style/runtime'; import {Provider} from 'react-aria-components/slots'; -import {Input as RACInput, InputProps as RACInputProps} from 'react-aria-components/Input'; // @ts-ignore import {StyleString} from '../style/types'; import {Text} from 'react-aria-components/Text'; @@ -187,11 +191,12 @@ export const FieldLabel = forwardRef(function FieldLabel( }); interface FieldGroupProps - extends Omit, UnsafeStyles { + extends Omit, UnsafeStyles { size?: 'S' | 'M' | 'L' | 'XL'; children: ReactNode; styles?: StyleString; shouldTurnOffFocusRing?: boolean; + prefix?: ReactNode; } const fieldGroupStyles = style({ @@ -240,7 +245,13 @@ export const FieldGroup = forwardRef(function FieldGroup( props: FieldGroupProps, ref: ForwardedRef ) { - let {shouldTurnOffFocusRing, ...otherProps} = props; + let {children, prefix, shouldTurnOffFocusRing, ...otherProps} = props; + let ctx = useContext(InputContext); + let prefixId = useId(); + let newAriaLabelledby = ctx?.['aria-labelledby']; + if (prefix) { + newAriaLabelledby = newAriaLabelledby ? `${newAriaLabelledby} ${prefixId}` : prefixId; + } return ( + }> + {props.prefix ? ( + + + {props.prefix} + + + ) : null} + + {ctx => ( + + {children} + + )} + + ); }); diff --git a/packages/@react-spectrum/s2/src/NumberField.tsx b/packages/@react-spectrum/s2/src/NumberField.tsx index eef3c41bee2..212f9611206 100644 --- a/packages/@react-spectrum/s2/src/NumberField.tsx +++ b/packages/@react-spectrum/s2/src/NumberField.tsx @@ -79,6 +79,11 @@ export interface NumberFieldProps * @default 'M' */ size?: 'S' | 'M' | 'L' | 'XL'; + /** + * The prefix to display in the NumberField. A non-interactive element that appears before the + * input. + */ + prefix?: ReactNode; } export const NumberFieldContext = @@ -239,6 +244,7 @@ export const NumberField = forwardRef(function NumberField( {label} {ctx => ( + value={{ + ...ctx, + ref: mergeRefs((ctx as any)?.ref, inputRef) + }}> )} diff --git a/packages/@react-spectrum/s2/src/TextField.tsx b/packages/@react-spectrum/s2/src/TextField.tsx index 98850c5b473..8e5a578e9cb 100644 --- a/packages/@react-spectrum/s2/src/TextField.tsx +++ b/packages/@react-spectrum/s2/src/TextField.tsx @@ -14,15 +14,13 @@ import { TextArea as AriaTextArea, TextAreaContext as AriaTextAreaContext } from 'react-aria-components/TextArea'; -import {TextContext as AriaTextContext} from 'react-aria-components/Text'; import { TextField as AriaTextField, TextFieldProps as AriaTextFieldProps } from 'react-aria-components/TextField'; -import {centerBaseline} from './CenterBaseline'; -import {centerPadding, fontRelative, style} from '../style' with {type: 'macro'}; +import {centerPadding, style} from '../style' with {type: 'macro'}; import {composeRenderProps} from 'react-aria-components/composeRenderProps'; -import {ContextValue, DEFAULT_SLOT, Provider, useSlottedContext} from 'react-aria-components/slots'; +import {ContextValue, Provider, useSlottedContext} from 'react-aria-components/slots'; import { controlSize, field, @@ -48,11 +46,10 @@ import { SpectrumLabelableProps } from '@react-types/shared'; import {FormContext, useFormProps} from './Form'; -import {IconContext} from './Icon'; import {InputContext, InputProps} from 'react-aria-components/Input'; import {mergeRefs} from 'react-aria/mergeRefs'; import {StyleString} from '../style/types'; -import {Text, TextContext} from './Content'; +import {TextContext} from './Content'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface TextFieldRef< @@ -79,7 +76,8 @@ export interface TextFieldProps */ size?: 'S' | 'M' | 'L' | 'XL'; /** - * The prefix to display in the text field. Either a string or workflow icon. + * The prefix to display in the text field. + * A non-interactive element that appears before the input. */ prefix?: ReactNode; } @@ -194,55 +192,15 @@ export const TextFieldBase = forwardRef(function TextFieldBase( contextualHelp={props.contextualHelp}> {label} - + - {props.prefix ? ( - -
- {typeof props.prefix === 'string' ? {props.prefix} : props.prefix} -
-
- ) : null} {ctx => ( + value={{ + ...ctx, + ref: mergeRefs((ctx as any)?.ref, inputRef) + }}> {children} )} diff --git a/packages/@react-spectrum/s2/stories/ColorField.stories.tsx b/packages/@react-spectrum/s2/stories/ColorField.stories.tsx index 150fd589265..458110a0f55 100644 --- a/packages/@react-spectrum/s2/stories/ColorField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ColorField.stories.tsx @@ -10,12 +10,15 @@ * governing permissions and limitations under the License. */ -import {ColorField} from '../src/ColorField'; +import {Color} from 'react-aria-components/ColorField'; +import {ColorField, ColorFieldProps} from '../src/ColorField'; +import {ColorSwatch} from '../src/ColorSwatch'; import {Content, Footer, Heading, Text} from '../src/Content'; import {ContextualHelp} from '../src/ContextualHelp'; import {Link} from '../src/Link'; import type {Meta, StoryObj} from '@storybook/react'; +import {useState} from 'react'; const meta: Meta = { component: ColorField, @@ -70,3 +73,22 @@ export const ContextualHelpExample: Story = { label: 'Color' } }; + +function ColorSwatchExample(props: ColorFieldProps) { + let [color, setColor] = useState(null); + return ( + } + /> + ); +} + +export const WithPrefix: Story = { + render: args => , + args: { + label: 'Color' + } +}; diff --git a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx index 97819bb3cf4..a2f4250587d 100644 --- a/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx +++ b/packages/@react-spectrum/s2/stories/ComboBox.stories.tsx @@ -411,3 +411,19 @@ export const ComboboxInsideDialog: Story = { ), args: Example.args }; + +export const WithPrefix: Story = { + render: (args: ComboBoxProps) => ( + + Chocolate + Mint + Strawberry + Vanilla + Chocolate Chip Cookie Dough + + ), + args: { + prefix: , + label: 'User ice cream flavor' + } +}; diff --git a/packages/@react-spectrum/s2/stories/NumberField.stories.tsx b/packages/@react-spectrum/s2/stories/NumberField.stories.tsx index d2d8bd25051..2bce18560d4 100644 --- a/packages/@react-spectrum/s2/stories/NumberField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/NumberField.stories.tsx @@ -99,3 +99,17 @@ export const ContextualHelpExample: Story = { label: 'Quantity' } }; + +export const WithPrefix: Story = { + render: args => ( + + ), + args: { + label: 'Value', + placeholder: '0.00', + prefix: 'USD' + } +}; diff --git a/packages/@react-spectrum/s2/stories/TextField.stories.tsx b/packages/@react-spectrum/s2/stories/TextField.stories.tsx index d78223c5851..7627b43425e 100644 --- a/packages/@react-spectrum/s2/stories/TextField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TextField.stories.tsx @@ -10,7 +10,9 @@ * governing permissions and limitations under the License. */ +import {Avatar} from '../src/Avatar'; import {Button} from '../src/Button'; +import {ColorSwatch} from '../src/ColorSwatch'; import {Content, Footer, Heading, Text} from '../src/Content'; import {ContextualHelp} from '../src/ContextualHelp'; import {Form} from '../src/Form'; @@ -190,6 +192,18 @@ export const TextFieldWithAddons: StoryTextField = { } placeholder="username" /> + } + placeholder="username" + /> + } + placeholder="#FF00FF" + /> diff --git a/packages/@react-spectrum/s2/test/ColorField.test.tsx b/packages/@react-spectrum/s2/test/ColorField.test.tsx new file mode 100644 index 00000000000..81b3e2b0f4a --- /dev/null +++ b/packages/@react-spectrum/s2/test/ColorField.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright 2025 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 {ColorField} from '../src/ColorField'; +import {render} from '@react-spectrum/test-utils-internal'; + +describe('ColorField', () => { + it('should label the input with the prefix', () => { + let {getByRole} = render(); + + let input = getByRole('textbox'); + let labels = input.getAttribute('aria-labelledby')?.split(' '); + expect(document.getElementById(labels![1])).toHaveTextContent('Prefix'); + }); +}); diff --git a/packages/@react-spectrum/s2/test/Combobox.test.tsx b/packages/@react-spectrum/s2/test/Combobox.test.tsx index 76c335241a0..c3244e2f749 100644 --- a/packages/@react-spectrum/s2/test/Combobox.test.tsx +++ b/packages/@react-spectrum/s2/test/Combobox.test.tsx @@ -292,4 +292,18 @@ describe('Combobox', () => { await user.click(backdrop!); expect(dialogTester.dialog).toBeNull(); }); + + it('should label the input with the prefix', () => { + let {getByRole} = render( + + Item 1 + Item 2 + Item 3 + + ); + + let input = getByRole('combobox'); + let labels = input.getAttribute('aria-labelledby')?.split(' '); + expect(document.getElementById(labels![1])).toHaveTextContent('Prefix'); + }); }); diff --git a/packages/@react-spectrum/s2/test/NumberField.test.tsx b/packages/@react-spectrum/s2/test/NumberField.test.tsx new file mode 100644 index 00000000000..d05fc2f797c --- /dev/null +++ b/packages/@react-spectrum/s2/test/NumberField.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright 2025 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 {NumberField} from '../src/NumberField'; +import {render} from '@react-spectrum/test-utils-internal'; + +describe('NumberField', () => { + it('should label the input with the prefix', () => { + let {getByRole} = render(); + + let input = getByRole('textbox'); + let labels = input.getAttribute('aria-labelledby')?.split(' '); + expect(document.getElementById(labels![1])).toHaveTextContent('Prefix'); + }); +}); diff --git a/packages/@react-spectrum/s2/test/TextField.test.tsx b/packages/@react-spectrum/s2/test/TextField.test.tsx index 5fd453535f7..13bea50124e 100644 --- a/packages/@react-spectrum/s2/test/TextField.test.tsx +++ b/packages/@react-spectrum/s2/test/TextField.test.tsx @@ -11,7 +11,7 @@ */ import {fireEvent, render} from '@react-spectrum/test-utils-internal'; -import {TextArea} from '../src/TextField'; +import {TextArea, TextField} from '../src/TextField'; describe('TextField', () => { it('should focus textarea when tapping invalid icon', async () => { @@ -27,4 +27,12 @@ describe('TextField', () => { expect(document.activeElement).toBe(textarea); }); + + it('should label the input with the prefix', () => { + let {getByRole} = render(); + + let input = getByRole('textbox'); + let labels = input.getAttribute('aria-labelledby')?.split(' '); + expect(document.getElementById(labels![1])).toHaveTextContent('Prefix'); + }); }); diff --git a/packages/dev/s2-docs/pages/s2/ColorField.mdx b/packages/dev/s2-docs/pages/s2/ColorField.mdx index 30ded9256b5..531fd2537f5 100644 --- a/packages/dev/s2-docs/pages/s2/ColorField.mdx +++ b/packages/dev/s2-docs/pages/s2/ColorField.mdx @@ -27,22 +27,20 @@ Use the `value` or `defaultValue` prop to set the color value, and `onChange` to ```tsx render "use client"; import {ColorField, type Color} from '@react-spectrum/s2/ColorField'; +import {ColorSwatch} from '@react-spectrum/s2/ColorSwatch'; 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')); return ( -
- -
Current value: {value?.toString('hex')}
-
+ } /> ); } ``` diff --git a/packages/dev/s2-docs/pages/s2/ComboBox.mdx b/packages/dev/s2-docs/pages/s2/ComboBox.mdx index 3cbae5954a0..727c23cdb2f 100644 --- a/packages/dev/s2-docs/pages/s2/ComboBox.mdx +++ b/packages/dev/s2-docs/pages/s2/ComboBox.mdx @@ -34,6 +34,7 @@ import {ComboBox, ComboBoxItem} from '@react-spectrum/s2/ComboBox'; ```tsx render "use client"; import {ComboBox, ComboBoxItem} from '@react-spectrum/s2/ComboBox'; +import Binoculars from '@react-spectrum/s2/icons/Binoculars'; function Example() { let options = [ @@ -50,7 +51,7 @@ function Example() { return ( /*- begin highlight -*/ - + }> {(item) => {item.name}} /*- end highlight -*/ diff --git a/packages/dev/s2-docs/pages/s2/NumberField.mdx b/packages/dev/s2-docs/pages/s2/NumberField.mdx index 748f53d7aa2..ecd008d527e 100644 --- a/packages/dev/s2-docs/pages/s2/NumberField.mdx +++ b/packages/dev/s2-docs/pages/s2/NumberField.mdx @@ -110,6 +110,27 @@ import {NumberField} from '@react-spectrum/s2/NumberField';
``` +## Prefix + +Use the `prefix` prop to display an additional element before the input in the number field. + +```tsx render +"use client"; +import {NumberField} from '@react-spectrum/s2/NumberField'; + +function Example() { + return ( + + ); +} +``` + ## Forms Use the `name` prop to submit the raw number value (not a formatted string) to the server. Set the `isRequired` prop to validate that the user enters a value, or implement custom client or server-side validation. See the [Forms](forms) guide to learn more. diff --git a/packages/dev/s2-docs/pages/s2/TextArea.mdx b/packages/dev/s2-docs/pages/s2/TextArea.mdx index cd514c816b0..63447f33033 100644 --- a/packages/dev/s2-docs/pages/s2/TextArea.mdx +++ b/packages/dev/s2-docs/pages/s2/TextArea.mdx @@ -52,7 +52,7 @@ function Example() { ## Prefix -Use the `prefix` prop to display a prefix in the text area. Either a string or workflow icon. +Use the `prefix` prop to display an additional element before the input in the text area. ```tsx render "use client"; diff --git a/packages/dev/s2-docs/pages/s2/TextField.mdx b/packages/dev/s2-docs/pages/s2/TextField.mdx index c7adbdd5c6f..bd8da618e7b 100644 --- a/packages/dev/s2-docs/pages/s2/TextField.mdx +++ b/packages/dev/s2-docs/pages/s2/TextField.mdx @@ -52,13 +52,14 @@ function Example() { ## Prefix -Use the `prefix` prop to display a prefix in the text field. Either a string or workflow icon. +Use the `prefix` prop to display an additional element before the input in the text field. ```tsx render "use client"; import {TextField} from '@react-spectrum/s2/TextField'; import MentionIcon from '@react-spectrum/s2/icons/Mention'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {Avatar} from '@react-spectrum/s2/Avatar'; function Example() { return ( @@ -66,10 +67,12 @@ function Example() { } placeholder="username" /> + } placeholder="contact@example.com" />
); } ``` + ## Forms Use the `name` prop to submit the text value to the server. Set the `isRequired`, `minLength`, `maxLength`, `pattern`, or `type` props to validate the value, or implement custom client or server-side validation. See the [Forms](forms) guide to learn more.