stream-chat-react-native-core 9.3.1-beta.4 → 9.3.1-beta.5

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 (144) hide show
  1. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js +4 -0
  2. package/lib/commonjs/components/Message/MessageItemView/MessageContent.js.map +1 -1
  3. package/lib/commonjs/components/Poll/Poll.js +21 -1
  4. package/lib/commonjs/components/Poll/Poll.js.map +1 -1
  5. package/lib/commonjs/components/Poll/components/PollButtons.js +39 -55
  6. package/lib/commonjs/components/Poll/components/PollButtons.js.map +1 -1
  7. package/lib/commonjs/components/Poll/components/PollOption.js +6 -19
  8. package/lib/commonjs/components/Poll/components/PollOption.js.map +1 -1
  9. package/lib/commonjs/components/Poll/contexts/PollUIStateContext.js +147 -0
  10. package/lib/commonjs/components/Poll/contexts/PollUIStateContext.js.map +1 -0
  11. package/lib/commonjs/components/Poll/contexts/index.js +15 -0
  12. package/lib/commonjs/components/Poll/contexts/index.js.map +1 -0
  13. package/lib/commonjs/components/Poll/hooks/useEndVote.js +48 -0
  14. package/lib/commonjs/components/Poll/hooks/useEndVote.js.map +1 -0
  15. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityActions.js +153 -0
  16. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityActions.js.map +1 -0
  17. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityLabel.js +64 -0
  18. package/lib/commonjs/components/Poll/hooks/usePollAccessibilityLabel.js.map +1 -0
  19. package/lib/commonjs/components/Poll/hooks/usePollState.js +2 -35
  20. package/lib/commonjs/components/Poll/hooks/usePollState.js.map +1 -1
  21. package/lib/commonjs/components/Poll/hooks/usePollVoteToggle.js +41 -0
  22. package/lib/commonjs/components/Poll/hooks/usePollVoteToggle.js.map +1 -0
  23. package/lib/commonjs/components/UIComponents/BottomSheetModal.js +40 -0
  24. package/lib/commonjs/components/UIComponents/BottomSheetModal.js.map +1 -1
  25. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js +15 -0
  26. package/lib/commonjs/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  27. package/lib/commonjs/i18n/ar.json +9 -1
  28. package/lib/commonjs/i18n/en.json +8 -0
  29. package/lib/commonjs/i18n/es.json +9 -1
  30. package/lib/commonjs/i18n/fr.json +9 -1
  31. package/lib/commonjs/i18n/he.json +9 -1
  32. package/lib/commonjs/i18n/hi.json +9 -1
  33. package/lib/commonjs/i18n/it.json +9 -1
  34. package/lib/commonjs/i18n/ja.json +9 -1
  35. package/lib/commonjs/i18n/ko.json +9 -1
  36. package/lib/commonjs/i18n/nl.json +9 -1
  37. package/lib/commonjs/i18n/pt-br.json +9 -1
  38. package/lib/commonjs/i18n/ru.json +9 -1
  39. package/lib/commonjs/i18n/tr.json +9 -1
  40. package/lib/commonjs/version.json +1 -1
  41. package/lib/module/components/Message/MessageItemView/MessageContent.js +4 -0
  42. package/lib/module/components/Message/MessageItemView/MessageContent.js.map +1 -1
  43. package/lib/module/components/Poll/Poll.js +21 -1
  44. package/lib/module/components/Poll/Poll.js.map +1 -1
  45. package/lib/module/components/Poll/components/PollButtons.js +39 -55
  46. package/lib/module/components/Poll/components/PollButtons.js.map +1 -1
  47. package/lib/module/components/Poll/components/PollOption.js +6 -19
  48. package/lib/module/components/Poll/components/PollOption.js.map +1 -1
  49. package/lib/module/components/Poll/contexts/PollUIStateContext.js +147 -0
  50. package/lib/module/components/Poll/contexts/PollUIStateContext.js.map +1 -0
  51. package/lib/module/components/Poll/contexts/index.js +15 -0
  52. package/lib/module/components/Poll/contexts/index.js.map +1 -0
  53. package/lib/module/components/Poll/hooks/useEndVote.js +48 -0
  54. package/lib/module/components/Poll/hooks/useEndVote.js.map +1 -0
  55. package/lib/module/components/Poll/hooks/usePollAccessibilityActions.js +153 -0
  56. package/lib/module/components/Poll/hooks/usePollAccessibilityActions.js.map +1 -0
  57. package/lib/module/components/Poll/hooks/usePollAccessibilityLabel.js +64 -0
  58. package/lib/module/components/Poll/hooks/usePollAccessibilityLabel.js.map +1 -0
  59. package/lib/module/components/Poll/hooks/usePollState.js +2 -35
  60. package/lib/module/components/Poll/hooks/usePollState.js.map +1 -1
  61. package/lib/module/components/Poll/hooks/usePollVoteToggle.js +41 -0
  62. package/lib/module/components/Poll/hooks/usePollVoteToggle.js.map +1 -0
  63. package/lib/module/components/UIComponents/BottomSheetModal.js +40 -0
  64. package/lib/module/components/UIComponents/BottomSheetModal.js.map +1 -1
  65. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js +15 -0
  66. package/lib/module/contexts/overlayContext/MessageOverlayHostLayer.js.map +1 -1
  67. package/lib/module/i18n/ar.json +9 -1
  68. package/lib/module/i18n/en.json +8 -0
  69. package/lib/module/i18n/es.json +9 -1
  70. package/lib/module/i18n/fr.json +9 -1
  71. package/lib/module/i18n/he.json +9 -1
  72. package/lib/module/i18n/hi.json +9 -1
  73. package/lib/module/i18n/it.json +9 -1
  74. package/lib/module/i18n/ja.json +9 -1
  75. package/lib/module/i18n/ko.json +9 -1
  76. package/lib/module/i18n/nl.json +9 -1
  77. package/lib/module/i18n/pt-br.json +9 -1
  78. package/lib/module/i18n/ru.json +9 -1
  79. package/lib/module/i18n/tr.json +9 -1
  80. package/lib/module/version.json +1 -1
  81. package/lib/typescript/components/Message/MessageItemView/MessageContent.d.ts.map +1 -1
  82. package/lib/typescript/components/Poll/Poll.d.ts.map +1 -1
  83. package/lib/typescript/components/Poll/components/PollButtons.d.ts.map +1 -1
  84. package/lib/typescript/components/Poll/components/PollOption.d.ts.map +1 -1
  85. package/lib/typescript/components/Poll/contexts/PollUIStateContext.d.ts +31 -0
  86. package/lib/typescript/components/Poll/contexts/PollUIStateContext.d.ts.map +1 -0
  87. package/lib/typescript/components/Poll/contexts/index.d.ts +2 -0
  88. package/lib/typescript/components/Poll/contexts/index.d.ts.map +1 -0
  89. package/lib/typescript/components/Poll/hooks/useEndVote.d.ts +7 -0
  90. package/lib/typescript/components/Poll/hooks/useEndVote.d.ts.map +1 -0
  91. package/lib/typescript/components/Poll/hooks/usePollAccessibilityActions.d.ts +25 -0
  92. package/lib/typescript/components/Poll/hooks/usePollAccessibilityActions.d.ts.map +1 -0
  93. package/lib/typescript/components/Poll/hooks/usePollAccessibilityLabel.d.ts +8 -0
  94. package/lib/typescript/components/Poll/hooks/usePollAccessibilityLabel.d.ts.map +1 -0
  95. package/lib/typescript/components/Poll/hooks/usePollState.d.ts.map +1 -1
  96. package/lib/typescript/components/Poll/hooks/usePollVoteToggle.d.ts +8 -0
  97. package/lib/typescript/components/Poll/hooks/usePollVoteToggle.d.ts.map +1 -0
  98. package/lib/typescript/components/UIComponents/BottomSheetModal.d.ts.map +1 -1
  99. package/lib/typescript/contexts/overlayContext/MessageOverlayHostLayer.d.ts.map +1 -1
  100. package/lib/typescript/i18n/ar.json +9 -1
  101. package/lib/typescript/i18n/en.json +8 -0
  102. package/lib/typescript/i18n/es.json +9 -1
  103. package/lib/typescript/i18n/fr.json +9 -1
  104. package/lib/typescript/i18n/he.json +9 -1
  105. package/lib/typescript/i18n/hi.json +9 -1
  106. package/lib/typescript/i18n/it.json +9 -1
  107. package/lib/typescript/i18n/ja.json +9 -1
  108. package/lib/typescript/i18n/ko.json +9 -1
  109. package/lib/typescript/i18n/nl.json +9 -1
  110. package/lib/typescript/i18n/pt-br.json +9 -1
  111. package/lib/typescript/i18n/ru.json +9 -1
  112. package/lib/typescript/i18n/tr.json +9 -1
  113. package/lib/typescript/utils/i18n/Streami18n.d.ts +8 -0
  114. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  115. package/package.json +1 -1
  116. package/src/components/Message/MessageItemView/MessageContent.tsx +4 -0
  117. package/src/components/Poll/Poll.tsx +29 -2
  118. package/src/components/Poll/components/PollButtons.tsx +37 -44
  119. package/src/components/Poll/components/PollOption.tsx +4 -13
  120. package/src/components/Poll/contexts/PollUIStateContext.tsx +105 -0
  121. package/src/components/Poll/contexts/index.ts +1 -0
  122. package/src/components/Poll/hooks/__tests__/usePollAccessibilityActions.test.tsx +358 -0
  123. package/src/components/Poll/hooks/__tests__/usePollAccessibilityLabel.test.tsx +142 -0
  124. package/src/components/Poll/hooks/useEndVote.ts +37 -0
  125. package/src/components/Poll/hooks/usePollAccessibilityActions.ts +191 -0
  126. package/src/components/Poll/hooks/usePollAccessibilityLabel.ts +75 -0
  127. package/src/components/Poll/hooks/usePollState.ts +3 -26
  128. package/src/components/Poll/hooks/usePollVoteToggle.ts +34 -0
  129. package/src/components/UIComponents/BottomSheetModal.tsx +44 -1
  130. package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx +17 -1
  131. package/src/i18n/ar.json +9 -1
  132. package/src/i18n/en.json +8 -0
  133. package/src/i18n/es.json +9 -1
  134. package/src/i18n/fr.json +9 -1
  135. package/src/i18n/he.json +9 -1
  136. package/src/i18n/hi.json +9 -1
  137. package/src/i18n/it.json +9 -1
  138. package/src/i18n/ja.json +9 -1
  139. package/src/i18n/ko.json +9 -1
  140. package/src/i18n/nl.json +9 -1
  141. package/src/i18n/pt-br.json +9 -1
  142. package/src/i18n/ru.json +9 -1
  143. package/src/i18n/tr.json +9 -1
  144. package/src/version.json +1 -1
@@ -0,0 +1,191 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import type { AccessibilityActionEvent, AccessibilityProps } from 'react-native';
4
+
5
+ import { PollOption, PollState, UserResponse } from 'stream-chat';
6
+
7
+ import { useEndVote } from './useEndVote';
8
+
9
+ import { usePollStateStore } from './usePollStateStore';
10
+
11
+ import { usePollVoteToggle } from './usePollVoteToggle';
12
+
13
+ import {
14
+ useChatContext,
15
+ useOwnCapabilitiesContext,
16
+ useTranslationContext,
17
+ } from '../../../contexts';
18
+ import { useAccessibilityContext } from '../../../contexts/accessibilityContext/AccessibilityContext';
19
+ import { useStableCallback } from '../../../hooks';
20
+ import { defaultPollOptionCount } from '../../../utils/constants';
21
+ import { usePollUIStateContext } from '../contexts/PollUIStateContext';
22
+
23
+ type AccessibilityAction = NonNullable<AccessibilityProps['accessibilityActions']>[number];
24
+ type OnAccessibilityAction = NonNullable<AccessibilityProps['onAccessibilityAction']>;
25
+
26
+ type PollA11yActionsSelectorResult = {
27
+ allowAnswers: boolean | undefined;
28
+ allowUserSuggestedOptions: boolean | undefined;
29
+ answersCount: number;
30
+ createdBy: UserResponse | null;
31
+ isClosed: boolean | undefined;
32
+ options: PollOption[];
33
+ };
34
+
35
+ const a11yActionsSelector = (state: PollState): PollA11yActionsSelectorResult => ({
36
+ allowAnswers: state.allow_answers,
37
+ allowUserSuggestedOptions: state.allow_user_suggested_options,
38
+ answersCount: state.answers_count,
39
+ createdBy: state.created_by,
40
+ isClosed: state.is_closed,
41
+ options: state.options,
42
+ });
43
+
44
+ export type UsePollAccessibilityActionsResult = {
45
+ accessibilityActions: readonly AccessibilityAction[] | undefined;
46
+ onAccessibilityAction: OnAccessibilityAction | undefined;
47
+ };
48
+
49
+ type ActionKind =
50
+ | { type: 'addComment' }
51
+ | { type: 'endVote' }
52
+ | { type: 'showAllComments' }
53
+ | { type: 'showAllOptions' }
54
+ | { type: 'suggestOption' }
55
+ | { type: 'viewResults' }
56
+ | { type: 'vote'; optionId: string };
57
+
58
+ /**
59
+ * Returns the `accessibilityActions` array and `onAccessibilityAction` handler
60
+ * for the poll composite container. Action set is gated by poll state +
61
+ * capabilities so each rotor entry corresponds to an interaction the user is
62
+ * actually allowed to perform. Returns `undefined`s when a11y is disabled.
63
+ *
64
+ * NOTE: We set both `name` and `label` to the same human-readable string on
65
+ * every action. iOS Fabric (new architecture, on by default in RN 0.81+) uses
66
+ * `accessibilityAction.name` as the string VoiceOver reads — `label` is
67
+ * ignored on that path (RCTViewComponentView.mm). iOS legacy (Paper) and
68
+ * Android both read `label`. Using the same value for both fields means the
69
+ * announcement is human-readable on every platform/architecture. Dispatch
70
+ * uses the action name as the lookup key into an internal kind map, so the
71
+ * raw strings never need to be exposed to consumers.
72
+ */
73
+ export const usePollAccessibilityActions = (): UsePollAccessibilityActionsResult => {
74
+ const { enabled } = useAccessibilityContext();
75
+ const { t } = useTranslationContext();
76
+ const { client } = useChatContext();
77
+ const { castPollVote } = useOwnCapabilitiesContext();
78
+ const { allowAnswers, allowUserSuggestedOptions, answersCount, createdBy, isClosed, options } =
79
+ usePollStateStore(a11yActionsSelector);
80
+ const { openAddComment, openAllComments, openAllOptions, openSuggestOption, openViewResults } =
81
+ usePollUIStateContext();
82
+ const toggleVote = usePollVoteToggle();
83
+ const endVote = useEndVote();
84
+
85
+ const canVote = !isClosed && !!castPollVote;
86
+ const canEnd = !isClosed && createdBy?.id === client.userID;
87
+ const canComment = !isClosed && !!allowAnswers;
88
+ const canSuggest = !isClosed && !!allowUserSuggestedOptions;
89
+ const hasMoreOptions = !!options && options.length > defaultPollOptionCount;
90
+ const hasComments = answersCount > 0;
91
+
92
+ const { accessibilityActions, actionKindByName } = useMemo<{
93
+ accessibilityActions: readonly AccessibilityAction[] | undefined;
94
+ actionKindByName: Map<string, ActionKind> | undefined;
95
+ }>(() => {
96
+ if (!enabled) {
97
+ return { accessibilityActions: undefined, actionKindByName: undefined };
98
+ }
99
+
100
+ const actions: AccessibilityAction[] = [];
101
+ const kindByName = new Map<string, ActionKind>();
102
+
103
+ const push = (name: string, kind: ActionKind) => {
104
+ actions.push({ label: name, name });
105
+ kindByName.set(name, kind);
106
+ };
107
+
108
+ push(t('View Results'), { type: 'viewResults' });
109
+
110
+ if (canVote && options) {
111
+ for (const option of options.slice(0, defaultPollOptionCount)) {
112
+ push(t('a11y/Vote on {{option}}', { option: option.text }), {
113
+ optionId: option.id,
114
+ type: 'vote',
115
+ });
116
+ }
117
+ }
118
+
119
+ if (hasMoreOptions) {
120
+ push(t('a11y/Show all options'), { type: 'showAllOptions' });
121
+ }
122
+
123
+ if (canEnd) {
124
+ push(t('a11y/End vote'), { type: 'endVote' });
125
+ }
126
+
127
+ if (canComment) {
128
+ push(t('Add a comment'), { type: 'addComment' });
129
+ }
130
+
131
+ if (canSuggest) {
132
+ push(t('Suggest an option'), { type: 'suggestOption' });
133
+ }
134
+
135
+ if (hasComments) {
136
+ push(t('View {{count}} comments', { count: answersCount }), { type: 'showAllComments' });
137
+ }
138
+
139
+ return { accessibilityActions: actions, actionKindByName: kindByName };
140
+ }, [
141
+ answersCount,
142
+ canComment,
143
+ canEnd,
144
+ canSuggest,
145
+ canVote,
146
+ enabled,
147
+ hasComments,
148
+ hasMoreOptions,
149
+ options,
150
+ t,
151
+ ]);
152
+
153
+ const onAccessibilityAction = useStableCallback((event: AccessibilityActionEvent) => {
154
+ const kind = actionKindByName?.get(event.nativeEvent.actionName);
155
+ if (!kind) return;
156
+
157
+ switch (kind.type) {
158
+ case 'viewResults':
159
+ openViewResults();
160
+ return;
161
+ case 'showAllOptions':
162
+ openAllOptions();
163
+ return;
164
+ case 'endVote':
165
+ void endVote();
166
+ return;
167
+ case 'addComment':
168
+ openAddComment();
169
+ return;
170
+ case 'suggestOption':
171
+ openSuggestOption();
172
+ return;
173
+ case 'showAllComments':
174
+ openAllComments();
175
+ return;
176
+ case 'vote':
177
+ void toggleVote(kind.optionId);
178
+ return;
179
+ default:
180
+ return;
181
+ }
182
+ });
183
+
184
+ return useMemo(
185
+ () => ({
186
+ accessibilityActions,
187
+ onAccessibilityAction: enabled ? onAccessibilityAction : undefined,
188
+ }),
189
+ [accessibilityActions, enabled, onAccessibilityAction],
190
+ );
191
+ };
@@ -0,0 +1,75 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { PollOption, PollState } from 'stream-chat';
4
+
5
+ import { usePollStateStore } from './usePollStateStore';
6
+
7
+ import { composeAccessibilityLabel } from '../../../a11y/a11yUtils';
8
+ import { useTranslationContext } from '../../../contexts';
9
+ import { useAccessibilityContext } from '../../../contexts/accessibilityContext/AccessibilityContext';
10
+ import { defaultPollOptionCount } from '../../../utils/constants';
11
+
12
+ type PollA11yLabelSelectorResult = {
13
+ enforceUniqueVote: boolean;
14
+ isClosed: boolean | undefined;
15
+ maxVotesAllowed: number;
16
+ name: string;
17
+ options: PollOption[];
18
+ voteCountsByOption: Record<string, number>;
19
+ };
20
+
21
+ const a11yLabelSelector = (state: PollState): PollA11yLabelSelectorResult => ({
22
+ enforceUniqueVote: state.enforce_unique_vote,
23
+ isClosed: state.is_closed,
24
+ maxVotesAllowed: state.max_votes_allowed,
25
+ name: state.name,
26
+ options: state.options,
27
+ voteCountsByOption: state.vote_counts_by_option,
28
+ });
29
+
30
+ /**
31
+ * Builds the composite accessibility label for a poll bubble: name, status,
32
+ * up to `defaultPollOptionCount` options with vote counts, an overflow hint,
33
+ * and the primary-tap hint. Returns `undefined` when a11y is disabled so the
34
+ * Poll container can leave its `accessibilityLabel` unset.
35
+ */
36
+ export const usePollAccessibilityLabel = (): string | undefined => {
37
+ const { enabled } = useAccessibilityContext();
38
+ const { t } = useTranslationContext();
39
+ const { enforceUniqueVote, isClosed, maxVotesAllowed, name, options, voteCountsByOption } =
40
+ usePollStateStore(a11yLabelSelector);
41
+
42
+ return useMemo(() => {
43
+ if (!enabled) return undefined;
44
+
45
+ let status: string;
46
+ if (isClosed) {
47
+ status = t('Poll has ended');
48
+ } else if (enforceUniqueVote) {
49
+ status = t('Select one');
50
+ } else if (maxVotesAllowed) {
51
+ status = t('Select up to {{count}}', { count: maxVotesAllowed });
52
+ } else {
53
+ status = t('Select one or more');
54
+ }
55
+
56
+ const visibleOptions = options?.slice(0, defaultPollOptionCount) ?? [];
57
+ const optionParts = visibleOptions.map((option) => {
58
+ const count = voteCountsByOption?.[option.id] ?? 0;
59
+ return `${option.text}: ${t('{{count}} votes', { count })}`;
60
+ });
61
+
62
+ const overflow =
63
+ options && options.length > defaultPollOptionCount
64
+ ? t('+{{count}} More Options', { count: options.length - defaultPollOptionCount })
65
+ : null;
66
+
67
+ return composeAccessibilityLabel(
68
+ name,
69
+ status,
70
+ ...optionParts,
71
+ overflow,
72
+ t('a11y/Activate to view results'),
73
+ );
74
+ }, [enabled, enforceUniqueVote, isClosed, maxVotesAllowed, name, options, t, voteCountsByOption]);
75
+ };
@@ -12,10 +12,10 @@ import {
12
12
  VotingVisibility,
13
13
  } from 'stream-chat';
14
14
 
15
+ import { useEndVote } from './useEndVote';
15
16
  import { usePollStateStore } from './usePollStateStore';
16
17
 
17
- import { usePollContext, useTranslationContext } from '../../../contexts';
18
- import { useNotificationApi } from '../../Notifications';
18
+ import { usePollContext } from '../../../contexts';
19
19
 
20
20
  export type UsePollStateSelectorReturnType = {
21
21
  allowAnswers: boolean | undefined;
@@ -63,8 +63,6 @@ const selector = (nextValue: PollState): UsePollStateSelectorReturnType => ({
63
63
 
64
64
  export const usePollState = (): UsePollStateReturnType => {
65
65
  const { message, poll } = usePollContext();
66
- const { addNotification } = useNotificationApi();
67
- const { t } = useTranslationContext();
68
66
  const {
69
67
  allowAnswers,
70
68
  allowUserSuggestedOptions,
@@ -94,28 +92,7 @@ export const usePollState = (): UsePollStateReturnType => {
94
92
  (answerText: string) => poll.addAnswer(answerText, message.id),
95
93
  [message.id, poll],
96
94
  );
97
- const endVote = useCallback(async () => {
98
- try {
99
- const response = await poll.close();
100
- addNotification({
101
- message: t('Poll ended'),
102
- options: { severity: 'success', type: 'api:poll:end:success' },
103
- origin: { emitter: 'PollActions' },
104
- });
105
- return response;
106
- } catch (error) {
107
- addNotification({
108
- message: t('Failed to end the poll'),
109
- options: {
110
- ...(error instanceof Error ? { originalError: error } : {}),
111
- severity: 'error',
112
- type: 'api:poll:end:failed',
113
- },
114
- origin: { emitter: 'PollActions' },
115
- });
116
- throw error;
117
- }
118
- }, [addNotification, poll, t]);
95
+ const endVote = useEndVote();
119
96
 
120
97
  return {
121
98
  addComment,
@@ -0,0 +1,34 @@
1
+ import { PollState } from 'stream-chat';
2
+
3
+ import { usePollStateStore } from './usePollStateStore';
4
+
5
+ import { usePollContext } from '../../../contexts';
6
+ import { useStableCallback } from '../../../hooks';
7
+ import { useNotificationApi } from '../../Notifications';
8
+
9
+ const ownVotesSelector = (state: PollState) => ({
10
+ ownVotesByOptionId: state.ownVotesByOptionId,
11
+ });
12
+
13
+ /**
14
+ * Returns a stable callback that toggles the current user's vote on a poll option
15
+ * by id: casts a vote if none exists, removes it if one does. Shared by the
16
+ * visible vote button and the rotor accessibility action so both paths use
17
+ * identical logic.
18
+ */
19
+ export const usePollVoteToggle = () => {
20
+ const { message, poll } = usePollContext();
21
+ const { runWithNotificationTarget } = useNotificationApi();
22
+ const { ownVotesByOptionId } = usePollStateStore(ownVotesSelector);
23
+
24
+ return useStableCallback(async (optionId: string) => {
25
+ await runWithNotificationTarget(async () => {
26
+ const existingVoteId = ownVotesByOptionId[optionId]?.id;
27
+ if (existingVoteId) {
28
+ await poll.removeVote(existingVoteId, message.id);
29
+ } else {
30
+ await poll.castVote(optionId, message.id);
31
+ }
32
+ });
33
+ });
34
+ };
@@ -7,6 +7,7 @@ import React, {
7
7
  useState,
8
8
  } from 'react';
9
9
  import {
10
+ AccessibilityActionEvent,
10
11
  EventSubscription,
11
12
  Keyboard,
12
13
  KeyboardEvent,
@@ -37,11 +38,13 @@ import {
37
38
  getBottomSheetTopSnapIndex,
38
39
  } from './BottomSheetModal.utils';
39
40
 
41
+ import { useA11yLabel } from '../../a11y/hooks/useA11yLabel';
40
42
  import { useResolvedModalAccessibilityProps } from '../../a11y/hooks/useResolvedModalAccessibilityProps';
41
43
  import { BottomSheetProvider } from '../../contexts/bottomSheetContext/BottomSheetContext';
42
44
  import { useTheme } from '../../contexts/themeContext/ThemeContext';
43
45
  import { useStableCallback } from '../../hooks';
44
46
  import { primitives } from '../../theme';
47
+ import { useAccessibilityAnnouncer } from '../Accessibility/useAccessibilityAnnouncer';
45
48
 
46
49
  export type BottomSheetModalProps = {
47
50
  /**
@@ -495,6 +498,37 @@ const BottomSheetModalInner = (props: PropsWithChildren<BottomSheetModalProps>)
495
498
 
496
499
  const modalA11yProps = useResolvedModalAccessibilityProps();
497
500
 
501
+ const announce = useAccessibilityAnnouncer();
502
+ const openAnnouncement = useA11yLabel(
503
+ 'a11y/Bottom sheet opened. Activate the close action or use the escape gesture to dismiss.',
504
+ );
505
+ const announcedOpenRef = useRef(false);
506
+ useEffect(() => {
507
+ if (!visible) {
508
+ announcedOpenRef.current = false;
509
+ return;
510
+ }
511
+ if (!openAnnouncement || announcedOpenRef.current) {
512
+ return;
513
+ }
514
+ const id = setTimeout(() => {
515
+ announce(openAnnouncement, 'polite');
516
+ announcedOpenRef.current = true;
517
+ }, 800);
518
+ return () => clearTimeout(id);
519
+ }, [visible, openAnnouncement, announce]);
520
+
521
+ const closeLabel = useA11yLabel('a11y/Close');
522
+ const closeAccessibilityActions = useMemo(
523
+ () => (closeLabel ? [{ label: closeLabel, name: 'activate' }] : undefined),
524
+ [closeLabel],
525
+ );
526
+ const onCloseAccessibilityAction = useStableCallback((event: AccessibilityActionEvent) => {
527
+ if (event.nativeEvent.actionName === 'activate') {
528
+ onClose();
529
+ }
530
+ });
531
+
498
532
  const bottomSheetModalContextValue = useMemo(
499
533
  () => ({
500
534
  close,
@@ -515,13 +549,22 @@ const BottomSheetModalInner = (props: PropsWithChildren<BottomSheetModalProps>)
515
549
  <Pressable onPress={onBackdropPress} style={StyleSheet.absoluteFill} />
516
550
 
517
551
  <Animated.View
552
+ onAccessibilityEscape={onClose}
518
553
  pointerEvents='box-none'
519
554
  style={[{ height: maxHeight }, sheetViewportAnimatedStyle]}
520
555
  {...modalA11yProps}
521
556
  >
522
557
  <GestureDetector gesture={panGesture}>
523
558
  <Animated.View style={[styles.container, { height: maxHeight }, container]}>
524
- <View style={[styles.handle, handle]} />
559
+ <View
560
+ accessibilityActions={closeAccessibilityActions}
561
+ accessibilityLabel={closeLabel}
562
+ accessibilityRole={closeLabel ? 'button' : undefined}
563
+ accessible={closeLabel ? true : undefined}
564
+ onAccessibilityAction={onCloseAccessibilityAction}
565
+ onAccessibilityTap={closeLabel ? onClose : undefined}
566
+ style={[styles.handle, handle]}
567
+ />
525
568
  <View style={[styles.contentContainer, contentContainer]}>
526
569
  {renderContent ? (
527
570
  <BottomSheetProvider value={bottomSheetModalContextValue}>
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo } from 'react';
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
2
  import {
3
3
  I18nManager,
4
4
  Platform,
@@ -23,6 +23,8 @@ import { PortalHost } from 'react-native-teleport';
23
23
 
24
24
  import { ClosingPortalHostsLayer } from './ClosingPortalHostsLayer';
25
25
 
26
+ import { useA11yLabel } from '../../a11y/hooks/useA11yLabel';
27
+ import { useAccessibilityAnnouncer } from '../../components/Accessibility/useAccessibilityAnnouncer';
26
28
  import {
27
29
  closeOverlay,
28
30
  finalizeCloseOverlay,
@@ -81,6 +83,20 @@ export const MessageOverlayHostLayer = () => {
81
83
 
82
84
  const isActive = !!id;
83
85
 
86
+ const announce = useAccessibilityAnnouncer();
87
+ const overlayOpenHint = useA11yLabel('a11y/Swipe right to go through different actions');
88
+ const announcedOpenRef = useRef(false);
89
+ useEffect(() => {
90
+ if (isActive) {
91
+ if (overlayOpenHint && !announcedOpenRef.current) {
92
+ announce(overlayOpenHint, 'polite');
93
+ announcedOpenRef.current = true;
94
+ }
95
+ } else {
96
+ announcedOpenRef.current = false;
97
+ }
98
+ }, [isActive, overlayOpenHint, announce]);
99
+
84
100
  const padding = 8;
85
101
  const minY = topInset + padding;
86
102
  const maxY = screenH - bottomInset - padding;
package/src/i18n/ar.json CHANGED
@@ -351,5 +351,13 @@
351
351
  "{{ user }} has been unmuted": "تم إلغاء كتم {{ user }}",
352
352
  "size limit": "حد الحجم",
353
353
  "unknown error": "خطأ غير معروف",
354
- "unsupported file type": "نوع ملف غير مدعوم"
354
+ "unsupported file type": "نوع ملف غير مدعوم",
355
+ "a11y/Activate to view results": "فعّل لعرض النتائج",
356
+ "a11y/End vote": "إنهاء التصويت",
357
+ "a11y/Show all options": "إظهار جميع الخيارات",
358
+ "a11y/Vote on {{option}}": "صوّت على {{option}}",
359
+ "a11y/Double tap and hold to activate contextual menu": "انقر نقرًا مزدوجًا مع الاستمرار لتفعيل قائمة السياق",
360
+ "a11y/Swipe right to go through different actions": "اسحب لليمين للتنقل بين الإجراءات المختلفة",
361
+ "a11y/Close": "Close",
362
+ "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."
355
363
  }
package/src/i18n/en.json CHANGED
@@ -308,6 +308,14 @@
308
308
  "a11y/Stop voice recording": "Stop voice recording",
309
309
  "a11y/Notifications": "Notifications",
310
310
  "a11y/Dismiss notification": "Dismiss notification",
311
+ "a11y/Activate to view results": "Activate to view results",
312
+ "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.",
313
+ "a11y/Close": "Close",
314
+ "a11y/Double tap and hold to activate contextual menu": "Double tap and hold to activate contextual menu",
315
+ "a11y/End vote": "End vote",
316
+ "a11y/Show all options": "Show all options",
317
+ "a11y/Swipe right to go through different actions": "Swipe right to go through different actions",
318
+ "a11y/Vote on {{option}}": "Vote on {{option}}",
311
319
  "Attachment upload blocked due to {{reason}}": "Attachment upload blocked due to {{reason}}",
312
320
  "Attachment upload failed due to {{reason}}": "Attachment upload failed due to {{reason}}",
313
321
  "Command not available": "Command not available",
package/src/i18n/es.json CHANGED
@@ -351,5 +351,13 @@
351
351
  "{{ user }} has been unmuted": "{{ user }} ya no está silenciado",
352
352
  "size limit": "límite de tamaño",
353
353
  "unknown error": "error desconocido",
354
- "unsupported file type": "tipo de archivo no compatible"
354
+ "unsupported file type": "tipo de archivo no compatible",
355
+ "a11y/Activate to view results": "Activa para ver los resultados",
356
+ "a11y/End vote": "Finalizar votación",
357
+ "a11y/Show all options": "Mostrar todas las opciones",
358
+ "a11y/Vote on {{option}}": "Votar por {{option}}",
359
+ "a11y/Double tap and hold to activate contextual menu": "Toca dos veces y mantén pulsado para activar el menú contextual",
360
+ "a11y/Swipe right to go through different actions": "Desliza a la derecha para recorrer las diferentes acciones",
361
+ "a11y/Close": "Close",
362
+ "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."
355
363
  }
package/src/i18n/fr.json CHANGED
@@ -351,5 +351,13 @@
351
351
  "{{ user }} has been unmuted": "{{ user }} n’est plus en sourdine",
352
352
  "size limit": "limite de taille",
353
353
  "unknown error": "erreur inconnue",
354
- "unsupported file type": "type de fichier non pris en charge"
354
+ "unsupported file type": "type de fichier non pris en charge",
355
+ "a11y/Activate to view results": "Activer pour voir les résultats",
356
+ "a11y/End vote": "Terminer le vote",
357
+ "a11y/Show all options": "Afficher toutes les options",
358
+ "a11y/Vote on {{option}}": "Voter pour {{option}}",
359
+ "a11y/Double tap and hold to activate contextual menu": "Appuyez deux fois et maintenez pour activer le menu contextuel",
360
+ "a11y/Swipe right to go through different actions": "Glissez vers la droite pour parcourir les différentes actions",
361
+ "a11y/Close": "Close",
362
+ "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."
355
363
  }
package/src/i18n/he.json CHANGED
@@ -351,5 +351,13 @@
351
351
  "{{ user }} has been unmuted": "{{ user }} כבר לא מושתק/ת",
352
352
  "size limit": "מגבלת גודל",
353
353
  "unknown error": "שגיאה לא ידועה",
354
- "unsupported file type": "סוג קובץ לא נתמך"
354
+ "unsupported file type": "סוג קובץ לא נתמך",
355
+ "a11y/Activate to view results": "הפעל כדי לראות את התוצאות",
356
+ "a11y/End vote": "סיים הצבעה",
357
+ "a11y/Show all options": "הצג את כל האפשרויות",
358
+ "a11y/Vote on {{option}}": "הצבע עבור {{option}}",
359
+ "a11y/Double tap and hold to activate contextual menu": "הקש פעמיים והחזק כדי להפעיל את התפריט ההקשרי",
360
+ "a11y/Swipe right to go through different actions": "החלק ימינה כדי לעבור בין הפעולות השונות",
361
+ "a11y/Close": "Close",
362
+ "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."
355
363
  }
package/src/i18n/hi.json CHANGED
@@ -351,5 +351,13 @@
351
351
  "{{ user }} has been unmuted": "{{ user }} को अनम्यूट किया गया",
352
352
  "size limit": "आकार सीमा",
353
353
  "unknown error": "अज्ञात त्रुटि",
354
- "unsupported file type": "असमर्थित फ़ाइल प्रकार"
354
+ "unsupported file type": "असमर्थित फ़ाइल प्रकार",
355
+ "a11y/Activate to view results": "परिणाम देखने के लिए सक्रिय करें",
356
+ "a11y/End vote": "मतदान समाप्त करें",
357
+ "a11y/Show all options": "सभी विकल्प दिखाएं",
358
+ "a11y/Vote on {{option}}": "{{option}} पर वोट करें",
359
+ "a11y/Double tap and hold to activate contextual menu": "संदर्भ मेनू सक्रिय करने के लिए दो बार टैप करें और होल्ड करें",
360
+ "a11y/Swipe right to go through different actions": "विभिन्न क्रियाओं के बीच जाने के लिए दाएं स्वाइप करें",
361
+ "a11y/Close": "Close",
362
+ "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."
355
363
  }
package/src/i18n/it.json CHANGED
@@ -351,5 +351,13 @@
351
351
  "{{ user }} has been unmuted": "{{ user }} non è più silenziato",
352
352
  "size limit": "limite di dimensione",
353
353
  "unknown error": "errore sconosciuto",
354
- "unsupported file type": "tipo di file non supportato"
354
+ "unsupported file type": "tipo di file non supportato",
355
+ "a11y/Activate to view results": "Attiva per vedere i risultati",
356
+ "a11y/End vote": "Termina sondaggio",
357
+ "a11y/Show all options": "Mostra tutte le opzioni",
358
+ "a11y/Vote on {{option}}": "Vota per {{option}}",
359
+ "a11y/Double tap and hold to activate contextual menu": "Tocca due volte e tieni premuto per attivare il menu contestuale",
360
+ "a11y/Swipe right to go through different actions": "Scorri a destra per passare in rassegna le diverse azioni",
361
+ "a11y/Close": "Close",
362
+ "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."
355
363
  }
package/src/i18n/ja.json CHANGED
@@ -351,5 +351,13 @@
351
351
  "{{ user }} has been unmuted": "{{ user }} のミュートを解除しました",
352
352
  "size limit": "サイズ制限",
353
353
  "unknown error": "不明なエラー",
354
- "unsupported file type": "サポートされていないファイル形式"
354
+ "unsupported file type": "サポートされていないファイル形式",
355
+ "a11y/Activate to view results": "結果を表示するには有効化",
356
+ "a11y/End vote": "投票を終了",
357
+ "a11y/Show all options": "すべてのオプションを表示",
358
+ "a11y/Vote on {{option}}": "{{option}}に投票",
359
+ "a11y/Double tap and hold to activate contextual menu": "コンテキストメニューを表示するにはダブルタップして長押し",
360
+ "a11y/Swipe right to go through different actions": "右にスワイプして異なるアクションを切り替えます",
361
+ "a11y/Close": "Close",
362
+ "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."
355
363
  }
package/src/i18n/ko.json CHANGED
@@ -351,5 +351,13 @@
351
351
  "{{ user }} has been unmuted": "{{ user }}님의 음소거가 해제되었습니다",
352
352
  "size limit": "크기 제한",
353
353
  "unknown error": "알 수 없는 오류",
354
- "unsupported file type": "지원되지 않는 파일 형식"
354
+ "unsupported file type": "지원되지 않는 파일 형식",
355
+ "a11y/Activate to view results": "결과를 보려면 활성화",
356
+ "a11y/End vote": "투표 종료",
357
+ "a11y/Show all options": "모든 옵션 표시",
358
+ "a11y/Vote on {{option}}": "{{option}}에 투표",
359
+ "a11y/Double tap and hold to activate contextual menu": "컨텍스트 메뉴를 활성화하려면 두 번 탭하고 길게 누르세요",
360
+ "a11y/Swipe right to go through different actions": "다른 작업을 탐색하려면 오른쪽으로 스와이프하세요",
361
+ "a11y/Close": "Close",
362
+ "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."
355
363
  }
package/src/i18n/nl.json CHANGED
@@ -351,5 +351,13 @@
351
351
  "{{ user }} has been unmuted": "{{ user }} is niet meer gedempt",
352
352
  "size limit": "groottelimiet",
353
353
  "unknown error": "onbekende fout",
354
- "unsupported file type": "niet-ondersteund bestandstype"
354
+ "unsupported file type": "niet-ondersteund bestandstype",
355
+ "a11y/Activate to view results": "Activeer om resultaten te bekijken",
356
+ "a11y/End vote": "Stemming beëindigen",
357
+ "a11y/Show all options": "Alle opties weergeven",
358
+ "a11y/Vote on {{option}}": "Stem op {{option}}",
359
+ "a11y/Double tap and hold to activate contextual menu": "Dubbeltik en houd vast om het contextmenu te openen",
360
+ "a11y/Swipe right to go through different actions": "Veeg naar rechts om door verschillende acties te bladeren",
361
+ "a11y/Close": "Close",
362
+ "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."
355
363
  }
@@ -351,5 +351,13 @@
351
351
  "{{ user }} has been unmuted": "{{ user }} teve o silenciamento removido",
352
352
  "size limit": "limite de tamanho",
353
353
  "unknown error": "erro desconhecido",
354
- "unsupported file type": "tipo de arquivo não compatível"
354
+ "unsupported file type": "tipo de arquivo não compatível",
355
+ "a11y/Activate to view results": "Ative para ver os resultados",
356
+ "a11y/End vote": "Encerrar votação",
357
+ "a11y/Show all options": "Mostrar todas as opções",
358
+ "a11y/Vote on {{option}}": "Votar em {{option}}",
359
+ "a11y/Double tap and hold to activate contextual menu": "Toque duas vezes e segure para ativar o menu contextual",
360
+ "a11y/Swipe right to go through different actions": "Deslize para a direita para percorrer as diferentes ações",
361
+ "a11y/Close": "Close",
362
+ "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."
355
363
  }