From d41125d44fe63b033819636642c5132f9cca25f2 Mon Sep 17 00:00:00 2001 From: capJavert Date: Thu, 9 Jul 2026 15:16:20 +0000 Subject: [PATCH 1/4] fix: keep audience dropdown open and additive when sharing links Clicking an audience row in the Share-a-link composer previously reset the selection to just that item and collapsed the dropdown, forcing a re-open per squad. Make row selection additive (toggle) and preventDefault in onSelect so the menu stays open across selections, matching the checkbox behavior. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../post/composer/AudienceChip.spec.tsx | 108 +++++++++++++++--- .../components/post/composer/AudienceChip.tsx | 11 +- 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/packages/shared/src/components/post/composer/AudienceChip.spec.tsx b/packages/shared/src/components/post/composer/AudienceChip.spec.tsx index 939268ccce1..3a8791ff602 100644 --- a/packages/shared/src/components/post/composer/AudienceChip.spec.tsx +++ b/packages/shared/src/components/post/composer/AudienceChip.spec.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { Squad } from '../../../graphql/sources'; @@ -74,20 +74,47 @@ const audiences = [ designSquad, ]; +const StatefulHarness = ({ + initialSelectedIds, + onChange, +}: { + initialSelectedIds: string[]; + onChange: jest.Mock; +}) => { + const [selectedIds, setSelectedIds] = useState(initialSelectedIds); + return ( + { + onChange(ids); + setSelectedIds(ids); + }} + userAudienceId="user-1" + /> + ); +}; + const renderComponent = ({ selectedIds = ['user-1'], onChange = jest.fn(), + stateful = false, }: { selectedIds?: string[]; onChange?: jest.Mock; + stateful?: boolean; } = {}) => { render( - , + stateful ? ( + + ) : ( + + ), ); return { onChange }; @@ -109,17 +136,38 @@ describe('AudienceChip', () => { ).toBeInTheDocument(); }); - it('single-selects an audience when clicking the row', async () => { + it('adds squads additively on row clicks and keeps the menu open', async () => { + const { onChange } = renderComponent({ stateful: true }); + + await userEvent.click(screen.getByText('Frontend')); + expect(onChange).toHaveBeenLastCalledWith(['user-1', 'squad-frontend']); + + await userEvent.click(screen.getByText('Backend')); + expect(onChange).toHaveBeenLastCalledWith([ + 'user-1', + 'squad-frontend', + 'squad-backend', + ]); + + // menu options remain mounted and the trigger reflects the accumulated selection + expect(screen.getByText('Frontend')).toBeInTheDocument(); + expect(screen.getByText('Backend')).toBeInTheDocument(); + expect(screen.getByText('3 audiences')).toBeInTheDocument(); + }); + + it('deselects an already-selected squad on row click without closing', async () => { const { onChange } = renderComponent({ - selectedIds: ['user-1', 'squad-backend'], + selectedIds: ['user-1', 'squad-frontend'], + stateful: true, }); await userEvent.click(screen.getByText('Frontend')); - expect(onChange).toHaveBeenCalledWith(['squad-frontend']); + expect(onChange).toHaveBeenLastCalledWith(['user-1']); + expect(screen.getByText('Frontend')).toBeInTheDocument(); }); - it('multi-selects an audience when clicking the checkbox', async () => { + it('multi-selects an audience when clicking the checkbox exactly once', async () => { const { onChange } = renderComponent(); await userEvent.click( @@ -132,20 +180,42 @@ describe('AudienceChip', () => { expect(onChange).toHaveBeenCalledWith(['user-1', 'squad-frontend']); }); - it('keeps row single-select available when the multi-select checkbox is disabled', async () => { + it('enforces the 3-squad cap on row clicks', async () => { const { onChange } = renderComponent({ selectedIds: ['squad-frontend', 'squad-backend', 'squad-mobile'], - }); - const designCheckbox = screen.getByRole('checkbox', { - name: 'Toggle Design for multi-audience posting', + stateful: true, }); - expect(designCheckbox).toBeDisabled(); + expect( + screen.getByText('You can post to up to 3 squads'), + ).toBeInTheDocument(); - await userEvent.click(designCheckbox); + await userEvent.click(screen.getByText('Design')); expect(onChange).not.toHaveBeenCalled(); + }); - await userEvent.click(screen.getByText('Design')); - expect(onChange).toHaveBeenCalledWith(['squad-design']); + it('falls back to Everyone when the last squad is removed via row click', async () => { + const { onChange } = renderComponent({ + selectedIds: ['squad-frontend'], + stateful: true, + }); + + // 'Frontend' also renders as the trigger label, so target the menu option row + const frontendOptions = screen.getAllByText('Frontend'); + await userEvent.click(frontendOptions[frontendOptions.length - 1]); + + expect(onChange).toHaveBeenLastCalledWith(['user-1']); + }); + + it('keeps Everyone selected when it is the sole audience', async () => { + const { onChange } = renderComponent({ + selectedIds: ['user-1'], + stateful: true, + }); + + const everyoneOptions = screen.getAllByText('Everyone'); + await userEvent.click(everyoneOptions[everyoneOptions.length - 1]); + + expect(onChange).not.toHaveBeenCalled(); }); }); diff --git a/packages/shared/src/components/post/composer/AudienceChip.tsx b/packages/shared/src/components/post/composer/AudienceChip.tsx index 5e823faf075..8d6ff0c20e0 100644 --- a/packages/shared/src/components/post/composer/AudienceChip.tsx +++ b/packages/shared/src/components/post/composer/AudienceChip.tsx @@ -133,13 +133,6 @@ export const AudienceChip = ({ toggleSquad(option); }; - const selectSingleOption = (option: Squad) => { - if (!option.id) { - return; - } - onChange([option.id]); - }; - const handleReset = () => { if (userAudienceId) { onChange([userAudienceId]); @@ -223,11 +216,11 @@ export const AudienceChip = ({ { + event.preventDefault(); if (!option.id) { - event.preventDefault(); return; } - selectSingleOption(option); + toggleOption(option); }} className="!h-9 gap-2 !overflow-visible !px-2" > From 13ddf55120b66f5487acd8600aa82585b6a7ddd4 Mon Sep 17 00:00:00 2001 From: capJavert Date: Thu, 9 Jul 2026 15:26:00 +0000 Subject: [PATCH 2/4] test: make AudienceChip menu-stays-open assertions real and simplify harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DropdownMenu mock previously rendered content unconditionally and ignored preventDefault, so the "menu stays open" assertions passed even if the menu closed. Teach the mock the Radix contract (content renders only while open; selecting closes unless onSelect calls preventDefault) so the tests actually guard the fix. Drop the redundant stateful flag — always use the stateful harness — and extract openMenu/clickOption helpers. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../post/composer/AudienceChip.spec.tsx | 175 ++++++++++-------- 1 file changed, 102 insertions(+), 73 deletions(-) diff --git a/packages/shared/src/components/post/composer/AudienceChip.spec.tsx b/packages/shared/src/components/post/composer/AudienceChip.spec.tsx index 3a8791ff602..35c87a0025b 100644 --- a/packages/shared/src/components/post/composer/AudienceChip.spec.tsx +++ b/packages/shared/src/components/post/composer/AudienceChip.spec.tsx @@ -5,38 +5,78 @@ import type { Squad } from '../../../graphql/sources'; import { SourceMemberRole, SourceType } from '../../../graphql/sources'; import { AudienceChip } from './AudienceChip'; -jest.mock('../../dropdown/DropdownMenu', () => ({ - DropdownMenu: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => - children, - DropdownMenuContent: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - DropdownMenuItem: ({ - children, - onSelect, - ...props - }: { - children: React.ReactNode; - onSelect?: (event: Event) => void; - }) => ( -
onSelect?.(event.nativeEvent)} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - onSelect?.(event.nativeEvent); - } - }} - {...props} - > - {children} -
- ), -})); +// Mirror the Radix contract we rely on: content renders only while open, the +// trigger opens it, and selecting an item closes the menu UNLESS onSelect calls +// preventDefault(). This lets the tests actually assert the menu stays open. +jest.mock('../../dropdown/DropdownMenu', () => { + const { createContext, useContext, cloneElement } = + jest.requireActual('react'); + const MenuContext = createContext<{ + open: boolean; + setOpen: (v: boolean) => void; + }>({ + open: false, + setOpen: () => {}, + }); + const select = ( + onSelect: ((event: Event) => void) | undefined, + nativeEvent: Event, + setOpen: (v: boolean) => void, + ) => { + onSelect?.(nativeEvent); + if (!nativeEvent.defaultPrevented) { + setOpen(false); + } + }; + return { + DropdownMenu: ({ + children, + open, + onOpenChange, + }: { + children: React.ReactNode; + open: boolean; + onOpenChange: (v: boolean) => void; + }) => ( + + {children} + + ), + DropdownMenuTrigger: ({ children }: { children: React.ReactElement }) => { + const { setOpen } = useContext(MenuContext); + return cloneElement(children, { onClick: () => setOpen(true) }); + }, + DropdownMenuContent: ({ children }: { children: React.ReactNode }) => { + const { open } = useContext(MenuContext); + return open ?
{children}
: null; + }, + DropdownMenuItem: ({ + children, + onSelect, + ...props + }: { + children: React.ReactNode; + onSelect?: (event: Event) => void; + }) => { + const { setOpen } = useContext(MenuContext); + return ( +
select(onSelect, event.nativeEvent, setOpen)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + select(onSelect, event.nativeEvent, setOpen); + } + }} + {...props} + > + {children} +
+ ); + }, + }; +}); const createAudience = ( id: string, @@ -74,6 +114,8 @@ const audiences = [ designSquad, ]; +// Owns selection state so the component behaves like the real controlled +// composer: each onChange feeds back into selectedIds and re-renders. const StatefulHarness = ({ initialSelectedIds, onChange, @@ -95,34 +137,32 @@ const StatefulHarness = ({ ); }; -const renderComponent = ({ +// Renders the chip and opens the menu (the menu is closed until the trigger is +// clicked, matching Radix), returning the onChange spy and the open menu. +const openMenu = async ({ selectedIds = ['user-1'], onChange = jest.fn(), - stateful = false, }: { selectedIds?: string[]; onChange?: jest.Mock; - stateful?: boolean; } = {}) => { render( - stateful ? ( - - ) : ( - - ), + , ); - + await userEvent.click(screen.getByRole('button', { name: /Posting to/ })); return { onChange }; }; +// The option row and the trigger can share a label (e.g. "Everyone"); the menu +// content renders after the trigger, so the option is the last match. +const clickOption = (label: string) => { + const matches = screen.getAllByText(label); + return userEvent.click(matches[matches.length - 1]); +}; + describe('AudienceChip', () => { - it('renders checkbox controls for audience options', () => { - renderComponent(); + it('renders checkbox controls for audience options', async () => { + await openMenu(); expect( screen.getByRole('checkbox', { @@ -137,38 +177,36 @@ describe('AudienceChip', () => { }); it('adds squads additively on row clicks and keeps the menu open', async () => { - const { onChange } = renderComponent({ stateful: true }); + const { onChange } = await openMenu(); - await userEvent.click(screen.getByText('Frontend')); + await clickOption('Frontend'); expect(onChange).toHaveBeenLastCalledWith(['user-1', 'squad-frontend']); - await userEvent.click(screen.getByText('Backend')); + await clickOption('Backend'); expect(onChange).toHaveBeenLastCalledWith([ 'user-1', 'squad-frontend', 'squad-backend', ]); - // menu options remain mounted and the trigger reflects the accumulated selection - expect(screen.getByText('Frontend')).toBeInTheDocument(); + // options are still mounted, so the menu stayed open across both clicks expect(screen.getByText('Backend')).toBeInTheDocument(); expect(screen.getByText('3 audiences')).toBeInTheDocument(); }); it('deselects an already-selected squad on row click without closing', async () => { - const { onChange } = renderComponent({ + const { onChange } = await openMenu({ selectedIds: ['user-1', 'squad-frontend'], - stateful: true, }); - await userEvent.click(screen.getByText('Frontend')); + await clickOption('Frontend'); expect(onChange).toHaveBeenLastCalledWith(['user-1']); expect(screen.getByText('Frontend')).toBeInTheDocument(); }); it('multi-selects an audience when clicking the checkbox exactly once', async () => { - const { onChange } = renderComponent(); + const { onChange } = await openMenu(); await userEvent.click( screen.getByRole('checkbox', { @@ -178,43 +216,34 @@ describe('AudienceChip', () => { expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith(['user-1', 'squad-frontend']); + expect(screen.getByText('Frontend')).toBeInTheDocument(); }); it('enforces the 3-squad cap on row clicks', async () => { - const { onChange } = renderComponent({ + const { onChange } = await openMenu({ selectedIds: ['squad-frontend', 'squad-backend', 'squad-mobile'], - stateful: true, }); expect( screen.getByText('You can post to up to 3 squads'), ).toBeInTheDocument(); - await userEvent.click(screen.getByText('Design')); + await clickOption('Design'); expect(onChange).not.toHaveBeenCalled(); }); it('falls back to Everyone when the last squad is removed via row click', async () => { - const { onChange } = renderComponent({ - selectedIds: ['squad-frontend'], - stateful: true, - }); + const { onChange } = await openMenu({ selectedIds: ['squad-frontend'] }); - // 'Frontend' also renders as the trigger label, so target the menu option row - const frontendOptions = screen.getAllByText('Frontend'); - await userEvent.click(frontendOptions[frontendOptions.length - 1]); + await clickOption('Frontend'); expect(onChange).toHaveBeenLastCalledWith(['user-1']); }); it('keeps Everyone selected when it is the sole audience', async () => { - const { onChange } = renderComponent({ - selectedIds: ['user-1'], - stateful: true, - }); + const { onChange } = await openMenu({ selectedIds: ['user-1'] }); - const everyoneOptions = screen.getAllByText('Everyone'); - await userEvent.click(everyoneOptions[everyoneOptions.length - 1]); + await clickOption('Everyone'); expect(onChange).not.toHaveBeenCalled(); }); From 737be28ce41e03ccf229776a8644b05ebf532275 Mon Sep 17 00:00:00 2001 From: capJavert Date: Fri, 10 Jul 2026 08:33:36 +0000 Subject: [PATCH 3/4] fix: make squad picker additive on the squads/create audience selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The share/create audience picker is implemented twice. Desktop with the SmartComposer experiment uses AudienceChip (already fixed); mobile/tablet — and anyone with the experiment off — falls back via CreatePostButton to the /squads/create page, which uses MultipleSourceSelect. There, a row click called selectSingleSource and reset the whole selection, so the reported "dropdown resets on every selection" persisted on mobile. Route row clicks through the existing additive toggleSource (matching the checkbox and AudienceChip), and remove the now-dead selectSingleSource. MultipleSourceSelect has pre-existing strict-null violations on unrelated lines (auth user/squad optionality); it is added to the strict migration skip list per the established convention, to be cleaned up separately. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 1 + .../post/write/MultipleSourceSelect.spec.tsx | 162 ++++++++++++++++++ .../post/write/MultipleSourceSelect.tsx | 11 +- scripts/typecheck-strict-changed.js | 6 + 4 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 packages/shared/src/components/post/write/MultipleSourceSelect.spec.tsx diff --git a/AGENTS.md b/AGENTS.md index 8d4efbdea51..751c2b721c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -454,6 +454,7 @@ When reviewing code (or writing code that will be reviewed): - **Keep scope tight in design iterations** - When adjusting UI, avoid unrelated behavioral/SEO changes in the same commit unless explicitly requested. - **Fix the calculation, not the collapse UX** - When a bug is a wrong value in a truncated/collapsed summary (e.g. per-company tenure on the collapsed profile), correct only the computation: derive the value from the complete dataset while rendering the exact same subset and keeping "Show More" visibility identical. Do NOT change the collapse unit (e.g. capping by positions vs by company groups) or the Show-More gating source as a side effect — those are visible UX shifts. Decouple "data summed/grouped over the full set" from "items rendered": compute over the full list, render the original truncated slice. Assert collapsed↔expanded value parity in a test. - **Confirm target surface before implementing UI fixes** - If a bug report names a specific component or screen, update only that target unless expansion is explicitly requested. +- **The post composer has two audience/squad pickers — fix both** - Audience selection during post creation / link sharing is implemented twice: `AudienceChip` (`packages/shared/src/components/post/composer/AudienceChip.tsx`) inside `SmartComposerModal`, and `MultipleSourceSelect` (`packages/shared/src/components/post/write/MultipleSourceSelect.tsx`) on the `/squads/create` page. `CreatePostButton` (`packages/shared/src/components/post/write/CreatePostButton.tsx`) only opens the `SmartComposer` modal when `isLaptop && evaluateSmartComposer()` (the `featureSmartComposer` flag); on mobile/tablet, or when the experiment is off, it navigates to `/squads/create`, so a fix that only touches `AudienceChip` still shows the old behavior on mobile. When changing audience-picker UX, mirror it across both components (they intentionally share the same row-click-is-additive-toggle model). - **Keep action spacing consistent in control headers** - When adding icon/action buttons near search fields or other controls, match existing horizontal gaps on both sides to avoid controls touching each other. - **Place feed promos in content flow unless explicitly sticky** - If a promo belongs between feed navigation and the feed list, render it in the feed/content layout so it pushes content down. Do not attach it to sticky nav with absolute positioning unless the requirement explicitly asks for overlay behavior. - **Use `searchChildren` prop for content above feed results on search pages** - In `MainFeedLayout`, page `children` render AFTER the `` component. To place banners/promos above feed results, pass them via the `searchChildren` prop (through `layoutProps` on the page component). Do NOT render them as page children — they will appear below all posts. Prefer reusing existing props over introducing new ones. diff --git a/packages/shared/src/components/post/write/MultipleSourceSelect.spec.tsx b/packages/shared/src/components/post/write/MultipleSourceSelect.spec.tsx new file mode 100644 index 00000000000..bdc38df967f --- /dev/null +++ b/packages/shared/src/components/post/write/MultipleSourceSelect.spec.tsx @@ -0,0 +1,162 @@ +import React, { useState } from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { MultipleSourceSelect } from './MultipleSourceSelect'; +import { defaultQueryClientTestingConfig } from '../../../../__tests__/helpers/tanstack-query'; +import type { Squad } from '../../../graphql/sources'; +import { + SourceMemberRole, + SourcePermissions, + SourceType, +} from '../../../graphql/sources'; + +const mockUser = { + id: 'user-1', + name: 'Chris', + username: 'chris', + image: 'https://daily.dev/chris.jpg', +}; + +const createSquad = (id: string, name: string): Squad => + ({ + id, + name, + handle: id, + type: SourceType.Squad, + image: `https://daily.dev/${id}.jpg`, + active: true, + permalink: `/squads/${id}`, + public: true, + membersCount: 1, + description: '', + memberPostingRole: SourceMemberRole.Member, + memberInviteRole: SourceMemberRole.Member, + moderationRequired: false, + moderationPostCount: 0, + currentMember: { + permissions: [SourcePermissions.Post], + }, + } as unknown as Squad); + +const mockSquads = [ + createSquad('squad-frontend', 'Frontend'), + createSquad('squad-backend', 'Backend'), + createSquad('squad-mobile', 'Mobile'), + createSquad('squad-design', 'Design'), +]; + +jest.mock('../../../contexts/AuthContext', () => ({ + useAuthContext: () => ({ user: mockUser, squads: mockSquads }), +})); + +// Render the popover body inline; the popover already stays open across +// selections, so the behavior under test is purely which ids get set. +jest.mock('../../../features/common/components/PopoverFormContainer', () => ({ + PopoverFormContainer: ({ + children, + onReset, + }: { + children: React.ReactNode; + onReset?: () => void; + }) => ( +
+ + {children} +
+ ), +})); + +// Owns selection state so each set feeds back in and re-renders, matching the +// real create-post form. +const Harness = ({ + initialSelectedIds, + setSelectedSourceIds, +}: { + initialSelectedIds: string[]; + setSelectedSourceIds: jest.Mock; +}) => { + const [selectedIds, setSelectedIds] = useState(initialSelectedIds); + return ( + { + setSelectedSourceIds(ids); + setSelectedIds(ids); + }} + /> + ); +}; + +const renderComponent = ({ + selectedIds = ['user-1'], + setSelectedSourceIds = jest.fn(), +}: { + selectedIds?: string[]; + setSelectedSourceIds?: jest.Mock; +} = {}) => { + render( + + + , + ); + return { setSelectedSourceIds }; +}; + +// A selected squad renders both a removable chip and its row, so target rows by +// their unique @handle text. +const clickRow = (handle: string) => userEvent.click(screen.getByText(handle)); + +describe('MultipleSourceSelect', () => { + it('adds sources additively on row click without resetting the selection', async () => { + const { setSelectedSourceIds } = renderComponent(); + + await clickRow('@squad-frontend'); + expect(setSelectedSourceIds).toHaveBeenLastCalledWith([ + 'user-1', + 'squad-frontend', + ]); + + await clickRow('@squad-backend'); + expect(setSelectedSourceIds).toHaveBeenLastCalledWith([ + 'user-1', + 'squad-frontend', + 'squad-backend', + ]); + }); + + it('toggles a source exactly once per row click', async () => { + const { setSelectedSourceIds } = renderComponent(); + + await clickRow('@squad-frontend'); + + expect(setSelectedSourceIds).toHaveBeenCalledTimes(1); + }); + + it('deselects an already-selected source on row click', async () => { + const { setSelectedSourceIds } = renderComponent({ + selectedIds: ['user-1', 'squad-frontend'], + }); + + await clickRow('@squad-frontend'); + + expect(setSelectedSourceIds).toHaveBeenLastCalledWith(['user-1']); + }); + + it('does not exceed the 3-squad cap on row click', async () => { + const { setSelectedSourceIds } = renderComponent({ + selectedIds: ['squad-frontend', 'squad-backend', 'squad-mobile'], + }); + + await clickRow('@squad-design'); + + expect(setSelectedSourceIds).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/shared/src/components/post/write/MultipleSourceSelect.tsx b/packages/shared/src/components/post/write/MultipleSourceSelect.tsx index eb3f099c6b0..66ee1bc07e1 100644 --- a/packages/shared/src/components/post/write/MultipleSourceSelect.tsx +++ b/packages/shared/src/components/post/write/MultipleSourceSelect.tsx @@ -251,13 +251,6 @@ export const MultipleSourceSelect = ({ ], ); - const selectSingleSource = useCallback( - (sourceId: string) => { - setSelectedSourceIds([sourceId]); - }, - [setSelectedSourceIds], - ); - const isUserSourceSelected = selectedSourceIds.includes(userSource.id); const sourceImage = isUserSourceSelected ? userSource.image @@ -312,7 +305,7 @@ export const MultipleSourceSelect = ({ checked={isUserSourceSelected} name="sources[]" onChange={() => toggleSource(userSource.id)} - onRowClick={() => selectSingleSource(userSource.id)} + onRowClick={() => toggleSource(userSource.id)} source={userSource} /> {!!squads.length && } @@ -327,7 +320,7 @@ export const MultipleSourceSelect = ({ } name="sources[]" onChange={() => toggleSource(squad.id)} - onRowClick={() => selectSingleSource(squad.id)} + onRowClick={() => toggleSource(squad.id)} source={squad} /> ); diff --git a/scripts/typecheck-strict-changed.js b/scripts/typecheck-strict-changed.js index e16ce391ed3..56894361276 100644 --- a/scripts/typecheck-strict-changed.js +++ b/scripts/typecheck-strict-changed.js @@ -157,6 +157,12 @@ const strictSkipList = new Set([ // mutable formRef typing on unrelated lines) predate this change and should // be addressed in a dedicated cleanup PR. 'packages/webapp/pages/posts/[id]/edit.tsx', + // Audience-select fix (ENG-1818) — this file was touched only to make the + // row click an additive toggle (matching AudienceChip) instead of a + // single-select reset. The pre-existing strict violations (auth user/squad + // optionality, squadsMapById index typing on unrelated lines) predate this + // change and should be addressed in a dedicated cleanup PR. + 'packages/shared/src/components/post/write/MultipleSourceSelect.tsx', ]); const changedFiles = getChangedTypescriptFiles().filter( From d6d98a77ed01578e73081340393e75312c50ae66 Mon Sep 17 00:00:00 2001 From: capJavert Date: Fri, 10 Jul 2026 09:40:32 +0000 Subject: [PATCH 4/4] fix: stop squads/create audience picker from collapsing on selection On mobile /squads/create the audience picker still closed on each selection. The row was a custom focusable role="button" that called preventDefault/ stopPropagation to drive selection, suppressing the native