stream-chat-react-native-core 9.3.1-beta.7 → 9.3.1-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/lib/commonjs/components/Accessibility/OverlayA11yShield.js +37 -0
  2. package/lib/commonjs/components/Accessibility/OverlayA11yShield.js.map +1 -0
  3. package/lib/commonjs/components/Attachment/Gallery.js +10 -0
  4. package/lib/commonjs/components/Attachment/Gallery.js.map +1 -1
  5. package/lib/commonjs/components/ChannelPreview/ChannelPreviewStatus.js +12 -3
  6. package/lib/commonjs/components/ChannelPreview/ChannelPreviewStatus.js.map +1 -1
  7. package/lib/commonjs/components/ImageGallery/ImageGallery.js +56 -1
  8. package/lib/commonjs/components/ImageGallery/ImageGallery.js.map +1 -1
  9. package/lib/commonjs/components/Message/MessageItemView/Headers/MessageReminderHeader.js +2 -0
  10. package/lib/commonjs/components/Message/MessageItemView/Headers/MessageReminderHeader.js.map +1 -1
  11. package/lib/commonjs/components/Message/MessageItemView/Headers/SentToChannelHeader.js +2 -0
  12. package/lib/commonjs/components/Message/MessageItemView/Headers/SentToChannelHeader.js.map +1 -1
  13. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js +26 -5
  14. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js.map +1 -1
  15. package/lib/commonjs/components/Message/MessageItemView/MessageReplies.js +7 -3
  16. package/lib/commonjs/components/Message/MessageItemView/MessageReplies.js.map +1 -1
  17. package/lib/commonjs/components/Message/MessageItemView/MessageRepliesAvatars.js +2 -0
  18. package/lib/commonjs/components/Message/MessageItemView/MessageRepliesAvatars.js.map +1 -1
  19. package/lib/commonjs/components/Message/MessageItemView/MessageTextContainer.js +16 -6
  20. package/lib/commonjs/components/Message/MessageItemView/MessageTextContainer.js.map +1 -1
  21. package/lib/commonjs/components/Message/MessageItemView/ReactionList/ReactionListItem.js +4 -0
  22. package/lib/commonjs/components/Message/MessageItemView/ReactionList/ReactionListItem.js.map +1 -1
  23. package/lib/commonjs/components/Message/hooks/useCreateMessageContext.js +6 -1
  24. package/lib/commonjs/components/Message/hooks/useCreateMessageContext.js.map +1 -1
  25. package/lib/commonjs/components/MessageList/InlineDateSeparator.js +8 -1
  26. package/lib/commonjs/components/MessageList/InlineDateSeparator.js.map +1 -1
  27. package/lib/commonjs/components/MessageMenu/ReactionButton.js +0 -7
  28. package/lib/commonjs/components/MessageMenu/ReactionButton.js.map +1 -1
  29. package/lib/commonjs/components/Poll/Poll.js +2 -0
  30. package/lib/commonjs/components/Poll/Poll.js.map +1 -1
  31. package/lib/commonjs/components/Poll/components/PollOption.js +6 -1
  32. package/lib/commonjs/components/Poll/components/PollOption.js.map +1 -1
  33. package/lib/commonjs/contexts/accessibilityContext/AccessibilityContext.js +6 -22
  34. package/lib/commonjs/contexts/accessibilityContext/AccessibilityContext.js.map +1 -1
  35. package/lib/commonjs/contexts/messageContext/MessageContext.js.map +1 -1
  36. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js +1 -0
  37. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  38. package/lib/commonjs/contexts/overlayContext/OverlayProvider.js +4 -1
  39. package/lib/commonjs/contexts/overlayContext/OverlayProvider.js.map +1 -1
  40. package/lib/commonjs/i18n/ar.json +7 -1
  41. package/lib/commonjs/i18n/en.json +7 -1
  42. package/lib/commonjs/i18n/es.json +7 -1
  43. package/lib/commonjs/i18n/fr.json +7 -1
  44. package/lib/commonjs/i18n/he.json +7 -1
  45. package/lib/commonjs/i18n/hi.json +7 -1
  46. package/lib/commonjs/i18n/it.json +7 -1
  47. package/lib/commonjs/i18n/ja.json +7 -1
  48. package/lib/commonjs/i18n/ko.json +7 -1
  49. package/lib/commonjs/i18n/nl.json +7 -1
  50. package/lib/commonjs/i18n/pt-br.json +7 -1
  51. package/lib/commonjs/i18n/ru.json +7 -1
  52. package/lib/commonjs/i18n/tr.json +7 -1
  53. package/lib/commonjs/state-store/image-gallery-state-store.js +13 -0
  54. package/lib/commonjs/state-store/image-gallery-state-store.js.map +1 -1
  55. package/lib/commonjs/utils/i18n/getDateString.js +26 -1
  56. package/lib/commonjs/utils/i18n/getDateString.js.map +1 -1
  57. package/lib/commonjs/version.json +1 -1
  58. package/lib/module/components/Accessibility/OverlayA11yShield.js +37 -0
  59. package/lib/module/components/Accessibility/OverlayA11yShield.js.map +1 -0
  60. package/lib/module/components/Attachment/Gallery.js +10 -0
  61. package/lib/module/components/Attachment/Gallery.js.map +1 -1
  62. package/lib/module/components/ChannelPreview/ChannelPreviewStatus.js +12 -3
  63. package/lib/module/components/ChannelPreview/ChannelPreviewStatus.js.map +1 -1
  64. package/lib/module/components/ImageGallery/ImageGallery.js +56 -1
  65. package/lib/module/components/ImageGallery/ImageGallery.js.map +1 -1
  66. package/lib/module/components/Message/MessageItemView/Headers/MessageReminderHeader.js +2 -0
  67. package/lib/module/components/Message/MessageItemView/Headers/MessageReminderHeader.js.map +1 -1
  68. package/lib/module/components/Message/MessageItemView/Headers/SentToChannelHeader.js +2 -0
  69. package/lib/module/components/Message/MessageItemView/Headers/SentToChannelHeader.js.map +1 -1
  70. package/lib/module/components/Message/MessageItemView/MessageContent.js +26 -5
  71. package/lib/module/components/Message/MessageItemView/MessageContent.js.map +1 -1
  72. package/lib/module/components/Message/MessageItemView/MessageReplies.js +7 -3
  73. package/lib/module/components/Message/MessageItemView/MessageReplies.js.map +1 -1
  74. package/lib/module/components/Message/MessageItemView/MessageRepliesAvatars.js +2 -0
  75. package/lib/module/components/Message/MessageItemView/MessageRepliesAvatars.js.map +1 -1
  76. package/lib/module/components/Message/MessageItemView/MessageTextContainer.js +16 -6
  77. package/lib/module/components/Message/MessageItemView/MessageTextContainer.js.map +1 -1
  78. package/lib/module/components/Message/MessageItemView/ReactionList/ReactionListItem.js +4 -0
  79. package/lib/module/components/Message/MessageItemView/ReactionList/ReactionListItem.js.map +1 -1
  80. package/lib/module/components/Message/hooks/useCreateMessageContext.js +6 -1
  81. package/lib/module/components/Message/hooks/useCreateMessageContext.js.map +1 -1
  82. package/lib/module/components/MessageList/InlineDateSeparator.js +8 -1
  83. package/lib/module/components/MessageList/InlineDateSeparator.js.map +1 -1
  84. package/lib/module/components/MessageMenu/ReactionButton.js +0 -7
  85. package/lib/module/components/MessageMenu/ReactionButton.js.map +1 -1
  86. package/lib/module/components/Poll/Poll.js +2 -0
  87. package/lib/module/components/Poll/Poll.js.map +1 -1
  88. package/lib/module/components/Poll/components/PollOption.js +6 -1
  89. package/lib/module/components/Poll/components/PollOption.js.map +1 -1
  90. package/lib/module/contexts/accessibilityContext/AccessibilityContext.js +6 -22
  91. package/lib/module/contexts/accessibilityContext/AccessibilityContext.js.map +1 -1
  92. package/lib/module/contexts/messageContext/MessageContext.js.map +1 -1
  93. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js +1 -0
  94. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  95. package/lib/module/contexts/overlayContext/OverlayProvider.js +4 -1
  96. package/lib/module/contexts/overlayContext/OverlayProvider.js.map +1 -1
  97. package/lib/module/i18n/ar.json +7 -1
  98. package/lib/module/i18n/en.json +7 -1
  99. package/lib/module/i18n/es.json +7 -1
  100. package/lib/module/i18n/fr.json +7 -1
  101. package/lib/module/i18n/he.json +7 -1
  102. package/lib/module/i18n/hi.json +7 -1
  103. package/lib/module/i18n/it.json +7 -1
  104. package/lib/module/i18n/ja.json +7 -1
  105. package/lib/module/i18n/ko.json +7 -1
  106. package/lib/module/i18n/nl.json +7 -1
  107. package/lib/module/i18n/pt-br.json +7 -1
  108. package/lib/module/i18n/ru.json +7 -1
  109. package/lib/module/i18n/tr.json +7 -1
  110. package/lib/module/state-store/image-gallery-state-store.js +13 -0
  111. package/lib/module/state-store/image-gallery-state-store.js.map +1 -1
  112. package/lib/module/utils/i18n/getDateString.js +26 -1
  113. package/lib/module/utils/i18n/getDateString.js.map +1 -1
  114. package/lib/module/version.json +1 -1
  115. package/lib/typescript/components/Accessibility/OverlayA11yShield.d.ts +20 -0
  116. package/lib/typescript/components/Accessibility/OverlayA11yShield.d.ts.map +1 -0
  117. package/lib/typescript/components/Attachment/Gallery.d.ts.map +1 -1
  118. package/lib/typescript/components/ChannelPreview/ChannelPreviewStatus.d.ts.map +1 -1
  119. package/lib/typescript/components/ImageGallery/ImageGallery.d.ts.map +1 -1
  120. package/lib/typescript/components/Message/MessageItemView/Headers/MessageReminderHeader.d.ts.map +1 -1
  121. package/lib/typescript/components/Message/MessageItemView/Headers/SentToChannelHeader.d.ts.map +1 -1
  122. package/lib/typescript/components/Message/MessageItemView/MessageContent.d.ts +1 -1
  123. package/lib/typescript/components/Message/MessageItemView/MessageContent.d.ts.map +1 -1
  124. package/lib/typescript/components/Message/MessageItemView/MessageReplies.d.ts.map +1 -1
  125. package/lib/typescript/components/Message/MessageItemView/MessageRepliesAvatars.d.ts.map +1 -1
  126. package/lib/typescript/components/Message/MessageItemView/MessageTextContainer.d.ts +1 -1
  127. package/lib/typescript/components/Message/MessageItemView/MessageTextContainer.d.ts.map +1 -1
  128. package/lib/typescript/components/Message/MessageItemView/ReactionList/ReactionListItem.d.ts.map +1 -1
  129. package/lib/typescript/components/Message/hooks/useCreateMessageContext.d.ts +1 -1
  130. package/lib/typescript/components/Message/hooks/useCreateMessageContext.d.ts.map +1 -1
  131. package/lib/typescript/components/MessageList/InlineDateSeparator.d.ts.map +1 -1
  132. package/lib/typescript/components/MessageMenu/ReactionButton.d.ts.map +1 -1
  133. package/lib/typescript/components/Poll/components/PollOption.d.ts.map +1 -1
  134. package/lib/typescript/contexts/accessibilityContext/AccessibilityContext.d.ts +32 -2
  135. package/lib/typescript/contexts/accessibilityContext/AccessibilityContext.d.ts.map +1 -1
  136. package/lib/typescript/contexts/messageContext/MessageContext.d.ts +8 -0
  137. package/lib/typescript/contexts/messageContext/MessageContext.d.ts.map +1 -1
  138. package/lib/typescript/contexts/overlayContext/MessageOverlayHostLayer.d.ts.map +1 -1
  139. package/lib/typescript/contexts/overlayContext/OverlayProvider.d.ts.map +1 -1
  140. package/lib/typescript/i18n/ar.json +7 -1
  141. package/lib/typescript/i18n/en.json +7 -1
  142. package/lib/typescript/i18n/es.json +7 -1
  143. package/lib/typescript/i18n/fr.json +7 -1
  144. package/lib/typescript/i18n/he.json +7 -1
  145. package/lib/typescript/i18n/hi.json +7 -1
  146. package/lib/typescript/i18n/it.json +7 -1
  147. package/lib/typescript/i18n/ja.json +7 -1
  148. package/lib/typescript/i18n/ko.json +7 -1
  149. package/lib/typescript/i18n/nl.json +7 -1
  150. package/lib/typescript/i18n/pt-br.json +7 -1
  151. package/lib/typescript/i18n/ru.json +7 -1
  152. package/lib/typescript/i18n/tr.json +7 -1
  153. package/lib/typescript/state-store/image-gallery-state-store.d.ts +5 -1
  154. package/lib/typescript/state-store/image-gallery-state-store.d.ts.map +1 -1
  155. package/lib/typescript/utils/i18n/Streami18n.d.ts +6 -0
  156. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  157. package/lib/typescript/utils/i18n/getDateString.d.ts +28 -0
  158. package/lib/typescript/utils/i18n/getDateString.d.ts.map +1 -1
  159. package/package.json +1 -1
  160. package/src/components/Accessibility/OverlayA11yShield.tsx +49 -0
  161. package/src/components/Accessibility/__tests__/OverlayA11yShield.test.tsx +83 -0
  162. package/src/components/Attachment/Gallery.tsx +14 -2
  163. package/src/components/Channel/__tests__/ownCapabilities.test.tsx +25 -2
  164. package/src/components/ChannelPreview/ChannelPreviewStatus.tsx +17 -3
  165. package/src/components/ImageGallery/ImageGallery.tsx +82 -4
  166. package/src/components/ImageGallery/__tests__/ImageGallery.test.tsx +13 -3
  167. package/src/components/ImageGallery/__tests__/ImageGalleryAdjustable.test.tsx +141 -0
  168. package/src/components/Message/MessageItemView/Headers/MessageReminderHeader.tsx +7 -1
  169. package/src/components/Message/MessageItemView/Headers/SentToChannelHeader.tsx +7 -1
  170. package/src/components/Message/MessageItemView/MessageContent.tsx +34 -15
  171. package/src/components/Message/MessageItemView/MessageReplies.tsx +9 -7
  172. package/src/components/Message/MessageItemView/MessageRepliesAvatars.tsx +5 -1
  173. package/src/components/Message/MessageItemView/MessageTextContainer.tsx +27 -4
  174. package/src/components/Message/MessageItemView/ReactionList/ReactionListItem.tsx +2 -0
  175. package/src/components/Message/MessageItemView/__tests__/Message.test.tsx +10 -3
  176. package/src/components/Message/MessageItemView/__tests__/MessageReplies.test.tsx +10 -2
  177. package/src/components/Message/hooks/useCreateMessageContext.ts +12 -1
  178. package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.tsx.snap +3 -0
  179. package/src/components/MessageInput/__tests__/__snapshots__/SendButton.test.tsx.snap +2 -0
  180. package/src/components/MessageList/InlineDateSeparator.tsx +10 -3
  181. package/src/components/MessageMenu/ReactionButton.tsx +0 -7
  182. package/src/components/MessageMenu/__tests__/ReactionButton.test.tsx +1 -14
  183. package/src/components/Poll/Poll.tsx +1 -1
  184. package/src/components/Poll/components/PollOption.tsx +7 -1
  185. package/src/components/Thread/__tests__/__snapshots__/Thread.test.tsx.snap +1 -0
  186. package/src/contexts/accessibilityContext/AccessibilityContext.tsx +60 -36
  187. package/src/contexts/messageContext/MessageContext.tsx +8 -0
  188. package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx +5 -1
  189. package/src/contexts/overlayContext/OverlayProvider.tsx +2 -1
  190. package/src/i18n/ar.json +7 -1
  191. package/src/i18n/en.json +7 -1
  192. package/src/i18n/es.json +7 -1
  193. package/src/i18n/fr.json +7 -1
  194. package/src/i18n/he.json +7 -1
  195. package/src/i18n/hi.json +7 -1
  196. package/src/i18n/it.json +7 -1
  197. package/src/i18n/ja.json +7 -1
  198. package/src/i18n/ko.json +7 -1
  199. package/src/i18n/nl.json +7 -1
  200. package/src/i18n/pt-br.json +7 -1
  201. package/src/i18n/ru.json +7 -1
  202. package/src/i18n/tr.json +7 -1
  203. package/src/state-store/__tests__/image-gallery-state-store.test.ts +1 -0
  204. package/src/state-store/image-gallery-state-store.ts +13 -1
  205. package/src/utils/i18n/getDateString.ts +57 -0
  206. package/src/version.json +1 -1
@@ -75,10 +75,18 @@ const MessageRepliesWithContext = (props: MessageRepliesPropsWithContext) => {
75
75
  return null;
76
76
  }
77
77
 
78
+ const replyCountLabel =
79
+ message.reply_count === 1
80
+ ? t('1 Reply')
81
+ : t('{{ replyCount }} Replies', { replyCount: message.reply_count });
82
+
78
83
  return (
79
84
  <View style={[styles.container, container]}>
80
85
  {alignment === 'left' ? connector : null}
81
86
  <Pressable
87
+ accessibilityHint={t('a11y/Double tap to view thread')}
88
+ accessibilityLabel={replyCountLabel}
89
+ accessibilityRole='button'
82
90
  disabled={preventPress}
83
91
  onLongPress={(event) => {
84
92
  if (onLongPress) {
@@ -114,13 +122,7 @@ const MessageRepliesWithContext = (props: MessageRepliesPropsWithContext) => {
114
122
  testID='message-replies'
115
123
  >
116
124
  <MessageRepliesAvatars />
117
- <Text style={[styles.messageRepliesText, messageRepliesText]}>
118
- {message.reply_count === 1
119
- ? t('1 Reply')
120
- : t('{{ replyCount }} Replies', {
121
- replyCount: message.reply_count,
122
- })}
123
- </Text>
125
+ <Text style={[styles.messageRepliesText, messageRepliesText]}>{replyCountLabel}</Text>
124
126
  </Pressable>
125
127
  {alignment === 'right' ? connector : null}
126
128
  </View>
@@ -24,7 +24,11 @@ export const MessageRepliesAvatarsWithContext = (props: MessageRepliesAvatarsPro
24
24
  const avatars = message?.thread_participants || [];
25
25
 
26
26
  return (
27
- <View style={avatarStackContainer}>
27
+ <View
28
+ accessibilityElementsHidden
29
+ importantForAccessibility='no-hide-descendants'
30
+ style={avatarStackContainer}
31
+ >
28
32
  <UserAvatarStack users={avatars} avatarSize='sm' maxVisible={3} overlap={0.4} />
29
33
  </View>
30
34
  );
@@ -31,7 +31,13 @@ export type MessageTextProps = MessageTextContainerProps & {
31
31
 
32
32
  export type MessageTextContainerPropsWithContext = Pick<
33
33
  MessageContextValue,
34
- 'message' | 'onLongPress' | 'onlyEmojis' | 'onPress' | 'preventPress' | 'isMyMessage'
34
+ | 'hasInteractiveAccessibilityContent'
35
+ | 'isMyMessage'
36
+ | 'message'
37
+ | 'onLongPress'
38
+ | 'onlyEmojis'
39
+ | 'onPress'
40
+ | 'preventPress'
35
41
  > &
36
42
  Pick<MessagesContextValue, 'markdownRules' | 'myMessageTheme' | 'messageTextNumberOfLines'> & {
37
43
  markdownStyles?: MarkdownStyle;
@@ -45,6 +51,7 @@ const MessageTextContainerWithContext = (props: MessageTextContainerPropsWithCon
45
51
  const theme = useTheme();
46
52
 
47
53
  const {
54
+ hasInteractiveAccessibilityContent,
48
55
  isMyMessage,
49
56
  markdownRules,
50
57
  markdownStyles: markdownStylesProp = {},
@@ -81,6 +88,8 @@ const MessageTextContainerWithContext = (props: MessageTextContainerPropsWithCon
81
88
 
82
89
  return (
83
90
  <View
91
+ accessible={hasInteractiveAccessibilityContent || undefined}
92
+ accessibilityRole={hasInteractiveAccessibilityContent ? 'text' : undefined}
84
93
  style={[styles.textContainer, textContainer, stylesProp.textContainer]}
85
94
  testID='message-text-container'
86
95
  >
@@ -113,18 +122,24 @@ const areEqual = (
113
122
  nextProps: MessageTextContainerPropsWithContext,
114
123
  ) => {
115
124
  const {
125
+ hasInteractiveAccessibilityContent: prevHasInteractiveAccessibilityContent,
116
126
  markdownStyles: prevMarkdownStyles,
117
127
  message: prevMessage,
118
128
  myMessageTheme: prevMyMessageTheme,
119
129
  onlyEmojis: prevOnlyEmojis,
120
130
  } = prevProps;
121
131
  const {
132
+ hasInteractiveAccessibilityContent: nextHasInteractiveAccessibilityContent,
122
133
  markdownStyles: nextMarkdownStyles,
123
134
  message: nextMessage,
124
135
  myMessageTheme: nextMyMessageTheme,
125
136
  onlyEmojis: nextOnlyEmojis,
126
137
  } = nextProps;
127
138
 
139
+ if (prevHasInteractiveAccessibilityContent !== nextHasInteractiveAccessibilityContent) {
140
+ return false;
141
+ }
142
+
128
143
  const messageStatusEqual = prevMessage.status === nextMessage.status;
129
144
  if (!messageStatusEqual) {
130
145
  return false;
@@ -182,16 +197,24 @@ const MemoizedMessageTextContainer = React.memo(
182
197
  export type MessageTextContainerProps = Partial<MessageTextContainerPropsWithContext>;
183
198
 
184
199
  export const MessageTextContainer = (props: MessageTextContainerProps) => {
185
- const { message, onLongPress, onlyEmojis, onPress, preventPress, isMyMessage } =
186
- useMessageContext();
200
+ const {
201
+ hasInteractiveAccessibilityContent,
202
+ isMyMessage,
203
+ message,
204
+ onLongPress,
205
+ onlyEmojis,
206
+ onPress,
207
+ preventPress,
208
+ } = useMessageContext();
187
209
  const { markdownRules, messageTextNumberOfLines, myMessageTheme } = useMessagesContext();
188
210
 
189
211
  return (
190
212
  <MemoizedMessageTextContainer
191
213
  {...{
214
+ hasInteractiveAccessibilityContent,
215
+ isMyMessage,
192
216
  markdownRules,
193
217
  message,
194
- isMyMessage,
195
218
  messageTextNumberOfLines,
196
219
  myMessageTheme,
197
220
  onLongPress,
@@ -72,6 +72,8 @@ export const ReactionListItem = (props: ReactionListItemProps) => {
72
72
 
73
73
  return (
74
74
  <ReactionListItemWrapper
75
+ accessibilityRole='button'
76
+ accessibilityState={{ selected }}
75
77
  disabled={preventPress}
76
78
  key={reaction.type}
77
79
  testID='reaction-list-item'
@@ -171,10 +171,17 @@ describe('Message', () => {
171
171
 
172
172
  fireEvent(getByTestId('custom-overlay-trigger'), 'longPress');
173
173
 
174
+ // Once the overlay opens, the host layer's `accessibilityViewIsModal`
175
+ // marks the chat-side subtree as hidden to RNTL. The overlay target's
176
+ // children are Portal-teleported but their React parent stays in the
177
+ // chat, so default visibility filtering excludes them. Pass
178
+ // `includeHiddenElements` to look past the modal-sibling heuristic.
174
179
  await waitFor(() => {
175
- expect(getByText('outside:normal')).toBeTruthy();
176
- expect(getByText('inside:overlay')).toBeTruthy();
177
- expect(getByTestId('custom-overlay-target-placeholder')).toBeTruthy();
180
+ expect(getByText('outside:normal', { includeHiddenElements: true })).toBeTruthy();
181
+ expect(getByText('inside:overlay', { includeHiddenElements: true })).toBeTruthy();
182
+ expect(
183
+ getByTestId('custom-overlay-target-placeholder', { includeHiddenElements: true }),
184
+ ).toBeTruthy();
178
185
  });
179
186
  });
180
187
  });
@@ -33,7 +33,11 @@ describe('MessageReplies', () => {
33
33
  );
34
34
 
35
35
  await waitFor(() => {
36
- expect(screen.getByTestId('message-replies')).toBeTruthy();
36
+ const pluralPressable = screen.getByTestId('message-replies');
37
+ expect(pluralPressable).toBeTruthy();
38
+ expect(pluralPressable.props.accessibilityRole).toBe('button');
39
+ expect(pluralPressable.props.accessibilityLabel).toBe('{{ replyCount }} Replies');
40
+ expect(pluralPressable.props.accessibilityHint).toBe('a11y/Double tap to view thread');
37
41
  expect(t).toHaveBeenCalledWith('{{ replyCount }} Replies', {
38
42
  replyCount: message.reply_count,
39
43
  });
@@ -57,7 +61,11 @@ describe('MessageReplies', () => {
57
61
 
58
62
  await waitFor(() => {
59
63
  expect(onPressMock).toHaveBeenCalled();
60
- expect(screen.getByTestId('message-replies')).toBeTruthy();
64
+ const singularPressable = screen.getByTestId('message-replies');
65
+ expect(singularPressable).toBeTruthy();
66
+ expect(singularPressable.props.accessibilityRole).toBe('button');
67
+ expect(singularPressable.props.accessibilityLabel).toBe('1 Reply');
68
+ expect(singularPressable.props.accessibilityHint).toBe('a11y/Double tap to view thread');
61
69
  expect(t).toHaveBeenCalledWith('1 Reply');
62
70
  expect(screen.getByText('1 Reply')).toBeTruthy();
63
71
  });
@@ -1,5 +1,6 @@
1
1
  import { useMemo, useRef } from 'react';
2
2
 
3
+ import { useAccessibilityContext } from '../../../contexts/accessibilityContext/AccessibilityContext';
3
4
  import type { MessageContextValue } from '../../../contexts/messageContext/MessageContext';
4
5
 
5
6
  import { stringifyMessage } from '../../../utils/utils';
@@ -58,7 +59,7 @@ export const useCreateMessageContext = ({
58
59
  threadList,
59
60
  videos,
60
61
  setQuotedMessage,
61
- }: MessageContextValue) => {
62
+ }: Omit<MessageContextValue, 'hasInteractiveAccessibilityContent'>) => {
62
63
  const stableGroupStyles = useStableRefValue(groupStyles);
63
64
  const reactionsValue = reactions.map(({ count, own, type }) => `${own}${type}${count}`).join();
64
65
  const stringifiedMessage = stringifyMessage({ message });
@@ -70,6 +71,14 @@ export const useCreateMessageContext = ({
70
71
  ? stringifyMessage({ includeReactions: false, message: message.quoted_message })
71
72
  : '';
72
73
 
74
+ // Resolved here (not at each consumer) so the boolean lives on MessageContext
75
+ // and downstream components (MessageContent, MessageTextContainer) read it
76
+ // directly. The predicate's identity is stable in the default case; an
77
+ // integrator override is expected to be stable too (documented on the config).
78
+ const { hasInteractiveAccessibilityContent: hasInteractiveAccessibilityContentPredicate } =
79
+ useAccessibilityContext();
80
+ const hasInteractiveAccessibilityContent = hasInteractiveAccessibilityContentPredicate(message);
81
+
73
82
  const messageContext: MessageContextValue = useMemo(
74
83
  () => ({
75
84
  actionsEnabled,
@@ -85,6 +94,7 @@ export const useCreateMessageContext = ({
85
94
  hasAttachmentActions,
86
95
  handleReaction,
87
96
  handleToggleReaction,
97
+ hasInteractiveAccessibilityContent,
88
98
  hasReactions,
89
99
  messageHasOnlySingleAttachment,
90
100
  images,
@@ -123,6 +133,7 @@ export const useCreateMessageContext = ({
123
133
  goToMessage,
124
134
  stableGroupStyles,
125
135
  hasAttachmentActions,
136
+ hasInteractiveAccessibilityContent,
126
137
  hasReactions,
127
138
  messageHasOnlySingleAttachment,
128
139
  lastGroupMessage,
@@ -818,6 +818,7 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
818
818
  </View>
819
819
  </View>
820
820
  <View
821
+ accessibilityViewIsModal={false}
821
822
  collapsable={false}
822
823
  pointerEvents="box-none"
823
824
  style={
@@ -1733,6 +1734,7 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
1733
1734
  </View>
1734
1735
  </View>
1735
1736
  <View
1737
+ accessibilityViewIsModal={false}
1736
1738
  collapsable={false}
1737
1739
  pointerEvents="box-none"
1738
1740
  style={
@@ -2648,6 +2650,7 @@ exports[`AttachButton should render an disabled AttachButton 1`] = `
2648
2650
  </View>
2649
2651
  </View>
2650
2652
  <View
2653
+ accessibilityViewIsModal={false}
2651
2654
  collapsable={false}
2652
2655
  pointerEvents="box-none"
2653
2656
  style={
@@ -816,6 +816,7 @@ exports[`SendButton should render a SendButton 1`] = `
816
816
  </View>
817
817
  </View>
818
818
  <View
819
+ accessibilityViewIsModal={false}
819
820
  collapsable={false}
820
821
  pointerEvents="box-none"
821
822
  style={
@@ -1729,6 +1730,7 @@ exports[`SendButton should render a disabled SendButton 1`] = `
1729
1730
  </View>
1730
1731
  </View>
1731
1732
  <View
1733
+ accessibilityViewIsModal={false}
1732
1734
  collapsable={false}
1733
1735
  pointerEvents="box-none"
1734
1736
  style={
@@ -4,7 +4,7 @@ import { StyleSheet, Text, View } from 'react-native';
4
4
  import { useTheme } from '../../contexts/themeContext/ThemeContext';
5
5
  import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
6
6
  import { primitives } from '../../theme';
7
- import { getDateString } from '../../utils/i18n/getDateString';
7
+ import { getDateString, getDateStringForA11y } from '../../utils/i18n/getDateString';
8
8
 
9
9
  /**
10
10
  * Props for the `InlineDateSeparator` component.
@@ -17,7 +17,7 @@ export type InlineDateSeparatorProps = {
17
17
  };
18
18
 
19
19
  export const InlineDateSeparator = ({ date }: InlineDateSeparatorProps) => {
20
- const { t, tDateTimeParser } = useTranslationContext();
20
+ const { t, tDateTimeParser, userLanguage } = useTranslationContext();
21
21
  const styles = useStyles();
22
22
 
23
23
  const dateString = useMemo(
@@ -31,9 +31,16 @@ export const InlineDateSeparator = ({ date }: InlineDateSeparatorProps) => {
31
31
  [date, t, tDateTimeParser],
32
32
  );
33
33
 
34
+ const a11yDateString = useMemo(
35
+ () => getDateStringForA11y({ date, tDateTimeParser, userLanguage }),
36
+ [date, tDateTimeParser, userLanguage],
37
+ );
38
+
34
39
  return (
35
40
  <View style={styles.container} testID='date-separator'>
36
- <Text style={styles.text}>{dateString}</Text>
41
+ <Text accessibilityLabel={a11yDateString} style={styles.text}>
42
+ {dateString}
43
+ </Text>
37
44
  </View>
38
45
  );
39
46
  };
@@ -49,17 +49,10 @@ export const ReactionButton = (props: ReactionButtonProps) => {
49
49
  () => <Icon size={reactionIconSize ?? 24} />,
50
50
  [Icon, reactionIconSize],
51
51
  );
52
- const selectedLabelState = selected ? 'selected' : 'unselected';
53
- const accessibilityLabelParams = useMemo(
54
- () => ({ selected: selectedLabelState, type }),
55
- [selectedLabelState, type],
56
- );
57
52
 
58
53
  return (
59
54
  <View style={styles.reactionButton}>
60
55
  <Button
61
- accessibilityLabelKey='a11y/reaction-button-{{type}}-{{selected}}'
62
- accessibilityLabelParams={accessibilityLabelParams}
63
56
  variant={'secondary'}
64
57
  type={'outline'}
65
58
  iconOnly={!count}
@@ -2,9 +2,8 @@ import React from 'react';
2
2
 
3
3
  import { Text } from 'react-native';
4
4
 
5
- import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
5
+ import { cleanup, fireEvent, render } from '@testing-library/react-native';
6
6
 
7
- import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvider';
8
7
  import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
9
8
  import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
10
9
  import { IconProps } from '../../../icons';
@@ -38,18 +37,6 @@ describe('ReactionButton', () => {
38
37
  expect(getByText('24')).toBeTruthy();
39
38
  });
40
39
 
41
- it('uses the released reaction button label when accessibility labels are translated', async () => {
42
- render(
43
- <OverlayProvider accessibility={{ enabled: true }}>
44
- <ReactionButton {...defaultProps} />
45
- </OverlayProvider>,
46
- );
47
-
48
- await waitFor(() => {
49
- expect(screen.getByLabelText('reaction-button-like-unselected')).toBeTruthy();
50
- });
51
- });
52
-
53
40
  it('should call onPress function with the correct reaction type when pressed', () => {
54
41
  const { getByRole } = render(
55
42
  <ThemeProvider theme={defaultTheme}>
@@ -50,7 +50,7 @@ export const PollHeader = () => {
50
50
  } = useTheme();
51
51
 
52
52
  return (
53
- <View style={styles.headerContainer}>
53
+ <View accessible accessibilityRole='text' style={styles.headerContainer}>
54
54
  <Text style={[styles.headerTitle, header.title]}>{name}</Text>
55
55
  <Text style={[styles.headerSubtitle, header.subtitle]}>{subtitle}</Text>
56
56
  </View>
@@ -92,6 +92,7 @@ export const PollAllOptions = ({
92
92
 
93
93
  export const PollOption = ({ option, showProgressBar = true, forceIncoming }: PollOptionProps) => {
94
94
  const { latestVotesByOption, voteCountsByOption, voteCount } = usePollState();
95
+ const { t } = useTranslationContext();
95
96
  const styles = useStyles();
96
97
 
97
98
  const relevantVotes = useMemo(
@@ -141,7 +142,12 @@ export const PollOption = ({ option, showProgressBar = true, forceIncoming }: Po
141
142
  />
142
143
  ) : null}
143
144
 
144
- <Text style={[styles.votesText, votesText]}>{voteCountsByOption[option.id] || 0}</Text>
145
+ <Text
146
+ accessibilityLabel={t('{{count}} votes', { count: votes })}
147
+ style={[styles.votesText, votesText]}
148
+ >
149
+ {votes}
150
+ </Text>
145
151
  </View>
146
152
  </View>
147
153
  {showProgressBar ? (
@@ -1017,6 +1017,7 @@ exports[`Thread should match thread snapshot 1`] = `
1017
1017
  testID="date-separator"
1018
1018
  >
1019
1019
  <Text
1020
+ accessibilityLabel="May 5, 2020"
1020
1021
  style={
1021
1022
  {
1022
1023
  "color": "#414552",
@@ -8,6 +8,8 @@ import React, {
8
8
  } from 'react';
9
9
  import { AccessibilityInfo } from 'react-native';
10
10
 
11
+ import type { LocalMessage } from 'stream-chat';
12
+
11
13
  import { AccessibilityAnnouncerContext } from '../../components/Accessibility/useAccessibilityAnnouncer';
12
14
  import type {
13
15
  AccessibilityAnnounce,
@@ -24,6 +26,39 @@ type TimeoutByPriority = {
24
26
  /** Tri-state for gesture-alternative toggles. */
25
27
  export type A11yMode = 'auto' | 'always' | 'never';
26
28
 
29
+ /**
30
+ * Resolved predicate stored on the context - consumers call `(message)` and
31
+ * receive the boolean directly.
32
+ */
33
+ export type HasInteractiveAccessibilityContent = (message: LocalMessage) => boolean;
34
+
35
+ /**
36
+ * Integrator facing override shape. Receives the SDK's baseline boolean as the
37
+ * second argument so overrides can extend rather than replace:
38
+ *
39
+ * ```
40
+ * hasInteractiveAccessibilityContent: (message, defaultValue) =>
41
+ * defaultValue || !!message.my_custom_field
42
+ * ```
43
+ *
44
+ * To replace fully, ignore the second argument. Must be stable across renders
45
+ * an unstable function will rerender every Message context consumer on every
46
+ * downstream render.
47
+ */
48
+ export type HasInteractiveAccessibilityContentConfig = (
49
+ message: LocalMessage,
50
+ defaultValue: boolean,
51
+ ) => boolean;
52
+
53
+ /** SDK baseline — true when the message renders interactive children. */
54
+ const defaultHasInteractiveAccessibilityContent: HasInteractiveAccessibilityContent = (message) =>
55
+ !!(
56
+ message.poll_id ||
57
+ message.quoted_message ||
58
+ message.attachments?.length ||
59
+ message.shared_location
60
+ );
61
+
27
62
  export type AccessibilityConfig = {
28
63
  /**
29
64
  * Master toggle. Default FALSE — integrators must opt in. When false, the SDK
@@ -45,10 +80,22 @@ export type AccessibilityConfig = {
45
80
  imageGalleryScreenReaderMode?: A11yMode;
46
81
  /** Message actions trigger. 'long-press' (no alt button), 'auto' (default — show button when SR is on), 'always-button'. */
47
82
  messageActionsTrigger?: 'long-press' | 'auto' | 'always-button';
83
+ /**
84
+ * Override the SDK's "this message has interactive children" decision.
85
+ * Will control what it means for a message to have interactive content.
86
+ * This affects the way we mark the message bubble as accessible and whether
87
+ * we let the children handle it on their own or whether we want the bubble to
88
+ * handle everything. See {@link HasInteractiveAccessibilityContentConfig}.
89
+ */
90
+ hasInteractiveAccessibilityContent?: HasInteractiveAccessibilityContentConfig;
48
91
  };
49
92
 
50
93
  /** Fully-resolved config — every field is populated with its default. */
51
- export type ResolvedAccessibilityConfig = Required<AccessibilityConfig>;
94
+ export type ResolvedAccessibilityConfig = Required<
95
+ Omit<AccessibilityConfig, 'hasInteractiveAccessibilityContent'>
96
+ > & {
97
+ hasInteractiveAccessibilityContent: HasInteractiveAccessibilityContent;
98
+ };
52
99
 
53
100
  export const accessibilityContextDefaultValue: ResolvedAccessibilityConfig = {
54
101
  announceConnectionState: true,
@@ -57,6 +104,7 @@ export const accessibilityContextDefaultValue: ResolvedAccessibilityConfig = {
57
104
  audioRecorderTapMode: 'auto',
58
105
  enabled: false,
59
106
  forceScreenReaderMode: false,
107
+ hasInteractiveAccessibilityContent: defaultHasInteractiveAccessibilityContent,
60
108
  imageGalleryScreenReaderMode: 'auto',
61
109
  messageActionsTrigger: 'auto',
62
110
  };
@@ -124,6 +172,7 @@ export const AccessibilityProvider = ({
124
172
  audioRecorderTapMode = accessibilityContextDefaultValue.audioRecorderTapMode,
125
173
  enabled = accessibilityContextDefaultValue.enabled,
126
174
  forceScreenReaderMode = accessibilityContextDefaultValue.forceScreenReaderMode,
175
+ hasInteractiveAccessibilityContent,
127
176
  imageGalleryScreenReaderMode = accessibilityContextDefaultValue.imageGalleryScreenReaderMode,
128
177
  messageActionsTrigger = accessibilityContextDefaultValue.messageActionsTrigger,
129
178
  } = value ?? {};
@@ -136,6 +185,13 @@ export const AccessibilityProvider = ({
136
185
  audioRecorderTapMode,
137
186
  enabled,
138
187
  forceScreenReaderMode,
188
+ hasInteractiveAccessibilityContent: hasInteractiveAccessibilityContent
189
+ ? (message) =>
190
+ hasInteractiveAccessibilityContent(
191
+ message,
192
+ accessibilityContextDefaultValue.hasInteractiveAccessibilityContent(message),
193
+ )
194
+ : accessibilityContextDefaultValue.hasInteractiveAccessibilityContent,
139
195
  imageGalleryScreenReaderMode,
140
196
  messageActionsTrigger,
141
197
  }),
@@ -146,6 +202,7 @@ export const AccessibilityProvider = ({
146
202
  audioRecorderTapMode,
147
203
  enabled,
148
204
  forceScreenReaderMode,
205
+ hasInteractiveAccessibilityContent,
149
206
  imageGalleryScreenReaderMode,
150
207
  messageActionsTrigger,
151
208
  ],
@@ -162,38 +219,5 @@ export const AccessibilityProvider = ({
162
219
  );
163
220
  };
164
221
 
165
- export const useAccessibilityContext = (): ResolvedAccessibilityConfig => {
166
- const {
167
- announceConnectionState,
168
- announceNewMessages,
169
- announceTypingIndicator,
170
- audioRecorderTapMode,
171
- enabled,
172
- forceScreenReaderMode,
173
- imageGalleryScreenReaderMode,
174
- messageActionsTrigger,
175
- } = useContext(AccessibilityContext);
176
-
177
- return useMemo(
178
- () => ({
179
- announceConnectionState,
180
- announceNewMessages,
181
- announceTypingIndicator,
182
- audioRecorderTapMode,
183
- enabled,
184
- forceScreenReaderMode,
185
- imageGalleryScreenReaderMode,
186
- messageActionsTrigger,
187
- }),
188
- [
189
- announceConnectionState,
190
- announceNewMessages,
191
- announceTypingIndicator,
192
- audioRecorderTapMode,
193
- enabled,
194
- forceScreenReaderMode,
195
- imageGalleryScreenReaderMode,
196
- messageActionsTrigger,
197
- ],
198
- );
199
- };
222
+ export const useAccessibilityContext = (): ResolvedAccessibilityConfig =>
223
+ useContext(AccessibilityContext);
@@ -45,6 +45,14 @@ export type MessageContextValue = {
45
45
  /** Whether or not any message attachment exposes actions. */
46
46
  hasAttachmentActions: boolean;
47
47
  handleToggleReaction: (reactionType: string) => Promise<void>;
48
+ /**
49
+ * Whether the message renders interactive children (poll options, attachment
50
+ * cells, the quoted-reply navigator, shared location) and the row should
51
+ * therefore drop its single focusstop so VO/TalkBack can drill into them.
52
+ * Resolved from `AccessibilityContext.hasInteractiveAccessibilityContent` -
53
+ * integrators override there to include their own custom content.
54
+ */
55
+ hasInteractiveAccessibilityContent: boolean;
48
56
  /** Whether or not message has reactions */
49
57
  hasReactions: boolean;
50
58
  /** Whether or not message has only a single attachment */
@@ -269,7 +269,11 @@ export const MessageOverlayHostLayer = () => {
269
269
 
270
270
  return (
271
271
  <GestureDetector gesture={tap}>
272
- <View pointerEvents='box-none' style={StyleSheet.absoluteFill}>
272
+ <View
273
+ accessibilityViewIsModal={isActive}
274
+ pointerEvents='box-none'
275
+ style={StyleSheet.absoluteFill}
276
+ >
273
277
  {isActive ? (
274
278
  <Animated.View pointerEvents='none' style={[StyleSheet.absoluteFill, backdropStyle]}>
275
279
  <OverlayBackground />
@@ -9,6 +9,7 @@ import { PortalProvider } from 'react-native-teleport';
9
9
  import { MessageOverlayHostLayer } from './MessageOverlayHostLayer';
10
10
  import { OverlayContext, OverlayProviderProps } from './OverlayContext';
11
11
 
12
+ import { OverlayA11yShield } from '../../components/Accessibility/OverlayA11yShield';
12
13
  import { ImageGallery } from '../../components/ImageGallery/ImageGallery';
13
14
  import { useStreami18n } from '../../hooks/useStreami18n';
14
15
 
@@ -107,7 +108,7 @@ export const OverlayProvider = (props: PropsWithChildren<OverlayProviderProps>)
107
108
  <ImageGalleryProvider value={imageGalleryProviderProps}>
108
109
  <ThemeProvider style={overlayContext.style}>
109
110
  <PortalProvider>
110
- {children}
111
+ <OverlayA11yShield>{children}</OverlayA11yShield>
111
112
  {overlay === 'gallery' && <ImageGallery overlayOpacity={overlayOpacity} />}
112
113
  <MessageOverlayHostLayer />
113
114
  </PortalProvider>
package/src/i18n/ar.json CHANGED
@@ -260,6 +260,7 @@
260
260
  "a11y/Direct chat with {{name}}": "محادثة مباشرة مع {{name}}",
261
261
  "a11y/Double tap to open": "انقر مرتين للفتح",
262
262
  "a11y/Double tap to view reactions": "انقر مرتين لعرض التفاعلات",
263
+ "a11y/Double tap to view thread": "انقر مرتين لعرض المحادثة الفرعية",
263
264
  "a11y/Editing message": "تعديل الرسالة",
264
265
  "a11y/Editing message: {{text}}": "تعديل الرسالة: {{text}}",
265
266
  "a11y/Last message {{date}}": "آخر رسالة {{date}}",
@@ -371,5 +372,10 @@
371
372
  "a11y/Swipe right to go through different actions": "اسحب لليمين للتنقل بين الإجراءات المختلفة",
372
373
  "a11y/Close": "Close",
373
374
  "a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.": "Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.",
374
- "a11y/{{count}} unread messages": "{{count}} رسائل غير مقروءة"
375
+ "a11y/{{count}} unread messages": "{{count}} رسائل غير مقروءة",
376
+ "a11y/Message from you": "رسالة منك",
377
+ "a11y/Message from {{sender}}": "رسالة من {{sender}}",
378
+ "a11y/Gallery Image": "صورة من المعرض",
379
+ "a11y/Gallery Video": "فيديو من المعرض",
380
+ "a11y/{{position}} of {{count}}": "{{position}} من {{count}}"
375
381
  }
package/src/i18n/en.json CHANGED
@@ -260,12 +260,15 @@
260
260
  "a11y/Direct chat with {{name}}": "Direct chat with {{name}}",
261
261
  "a11y/Double tap to open": "Double tap to open",
262
262
  "a11y/Double tap to view reactions": "Double tap to view reactions",
263
+ "a11y/Double tap to view thread": "Double tap to view thread",
263
264
  "a11y/Editing message": "Editing message",
264
265
  "a11y/Editing message: {{text}}": "Editing message: {{text}}",
265
266
  "a11y/Last message {{date}}": "Last message {{date}}",
266
267
  "a11y/Loading": "Loading",
267
268
  "a11y/Loading failed": "Loading failed",
268
269
  "a11y/Message actions": "Message actions",
270
+ "a11y/Message from you": "Message from you",
271
+ "a11y/Message from {{sender}}": "Message from {{sender}}",
269
272
  "a11y/Muted": "Muted",
270
273
  "a11y/New message from {{user}}": "New message from {{user}}",
271
274
  "a11y/Offline": "Offline",
@@ -371,5 +374,8 @@
371
374
  "size limit": "size limit",
372
375
  "unknown error": "unknown error",
373
376
  "unsupported file type": "unsupported file type",
374
- "a11y/{{count}} unread messages": "{{count}} unread messages"
377
+ "a11y/{{count}} unread messages": "{{count}} unread messages",
378
+ "a11y/Gallery Image": "Gallery image",
379
+ "a11y/Gallery Video": "Gallery video",
380
+ "a11y/{{position}} of {{count}}": "{{position}} of {{count}}"
375
381
  }