@planningcenter/chat-react-native 4.5.0-rc.1 → 4.5.0-rc.3

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 (38) hide show
  1. package/build/components/display/filter_dropdown.d.ts +6 -1
  2. package/build/components/display/filter_dropdown.d.ts.map +1 -1
  3. package/build/components/display/filter_dropdown.js +57 -35
  4. package/build/components/display/filter_dropdown.js.map +1 -1
  5. package/build/components/display/index.d.ts +1 -0
  6. package/build/components/display/index.d.ts.map +1 -1
  7. package/build/components/display/index.js +1 -0
  8. package/build/components/display/index.js.map +1 -1
  9. package/build/components/display/popover_menu_view.d.ts +8 -0
  10. package/build/components/display/popover_menu_view.d.ts.map +1 -0
  11. package/build/components/display/popover_menu_view.js +482 -0
  12. package/build/components/display/popover_menu_view.js.map +1 -0
  13. package/build/screens/conversations/components/filter_dropdown_list_header_component.d.ts.map +1 -1
  14. package/build/screens/conversations/components/filter_dropdown_list_header_component.js +138 -94
  15. package/build/screens/conversations/components/filter_dropdown_list_header_component.js.map +1 -1
  16. package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
  17. package/build/screens/conversations/conversations_screen.js +1 -2
  18. package/build/screens/conversations/conversations_screen.js.map +1 -1
  19. package/build/screens/design_system_screen.d.ts.map +1 -1
  20. package/build/screens/design_system_screen.js +289 -201
  21. package/build/screens/design_system_screen.js.map +1 -1
  22. package/build/utils/native_adapters/menu.d.ts +1 -2
  23. package/build/utils/native_adapters/menu.d.ts.map +1 -1
  24. package/build/utils/native_adapters/menu.js +2 -41
  25. package/build/utils/native_adapters/menu.js.map +1 -1
  26. package/package.json +2 -2
  27. package/src/components/display/__tests__/filter_dropdown.test.tsx +18 -2
  28. package/src/components/display/__tests__/popover_menu_view.test.tsx +63 -0
  29. package/src/components/display/filter_dropdown.tsx +11 -3
  30. package/src/components/display/index.ts +1 -0
  31. package/src/components/display/popover_menu_view.tsx +239 -0
  32. package/src/screens/conversations/__tests__/conversations_screen.test.tsx +2 -12
  33. package/src/screens/conversations/components/__tests__/filter_dropdown_list_header_component.test.tsx +45 -0
  34. package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +38 -12
  35. package/src/screens/conversations/conversations_screen.tsx +1 -2
  36. package/src/screens/design_system_screen.tsx +34 -1
  37. package/src/utils/native_adapters/menu.tsx +4 -18
  38. package/src/utils/native_adapters/__tests__/menu.test.tsx +0 -46
@@ -1,13 +1,13 @@
1
1
  import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
2
- import React, { useCallback, useMemo } from 'react'
3
- import { Alert, StyleSheet, View } from 'react-native'
2
+ import React, { useCallback, useMemo, useState } from 'react'
3
+ import { Alert, LayoutChangeEvent, StyleSheet, View } from 'react-native'
4
4
  import { FilterDropdown, FilterDropdownOption, TextButton } from '../../../components'
5
5
  import {
6
- useAtFontScaleBreakpoint,
7
6
  useCanBrowseArchivedConversations,
8
7
  useCanCreateConversations,
9
8
  useCurrentPersonCache,
10
9
  useDirectMessagesEligible,
10
+ useFontScale,
11
11
  useHasDirectMessages,
12
12
  useNewConversationEntry,
13
13
  useTheme,
@@ -25,6 +25,8 @@ import { ConversationsScreenProps } from '../conversations_screen'
25
25
  import { ListHeaderActionButton } from './list_header_action_button'
26
26
 
27
27
  const ROW_PADDING_HORIZONTAL = 16
28
+ const HEADING_ROW_GAP = 12
29
+ const DEFAULT_FONT_SCALE = 1
28
30
  const ALL_CONVERSATIONS_LABEL = 'All conversations'
29
31
  const ARCHIVED_LABEL = 'Archived'
30
32
 
@@ -205,11 +207,31 @@ export const FilterDropdownListHeaderComponent = () => {
205
207
 
206
208
  const showMoreFilters = canFilterByGroups || canFilterByTeams || canBrowseArchived
207
209
 
208
- const styles = useStyles()
210
+ const stacked = useFontScale() > DEFAULT_FONT_SCALE
211
+ const styles = useStyles({ stacked })
212
+
213
+ // The Android menu host measures the trigger at its intrinsic width, so flex shrinking never
214
+ // truncates the label. Cap it to the room left beside the action buttons instead.
215
+ const [rowWidth, setRowWidth] = useState<number>()
216
+ const [actionsWidth, setActionsWidth] = useState<number>()
217
+ const handleRowLayout = useCallback((event: LayoutChangeEvent) => {
218
+ setRowWidth(event.nativeEvent.layout.width)
219
+ }, [])
220
+ const handleActionsLayout = useCallback((event: LayoutChangeEvent) => {
221
+ setActionsWidth(event.nativeEvent.layout.width)
222
+ }, [])
223
+ const triggerMaxWidth =
224
+ !stacked && rowWidth !== undefined && actionsWidth !== undefined
225
+ ? rowWidth - ROW_PADDING_HORIZONTAL * 2 - actionsWidth - HEADING_ROW_GAP
226
+ : undefined
209
227
 
210
228
  return (
211
229
  <View style={styles.listHeader}>
212
- <View style={styles.headingRow}>
230
+ <View
231
+ style={styles.headingRow}
232
+ onLayout={handleRowLayout}
233
+ testID="conversations-filter-heading-row"
234
+ >
213
235
  <View style={styles.titleContainer}>
214
236
  <FilterDropdown
215
237
  options={options}
@@ -218,9 +240,14 @@ export const FilterDropdownListHeaderComponent = () => {
218
240
  onMoreFilters={handleOpenConversationFilters}
219
241
  showMoreFilters={showMoreFilters}
220
242
  triggerLabel={triggerLabel}
243
+ maxWidth={triggerMaxWidth}
221
244
  />
222
245
  </View>
223
- <View style={styles.actionsContainer}>
246
+ <View
247
+ style={styles.actionsContainer}
248
+ onLayout={handleActionsLayout}
249
+ testID="conversations-filter-actions"
250
+ >
224
251
  <ListHeaderActionButton
225
252
  name="general.cog"
226
253
  onPress={handleChatSettings}
@@ -265,9 +292,8 @@ export const FilterDropdownListHeaderComponent = () => {
265
292
  )
266
293
  }
267
294
 
268
- const useStyles = () => {
295
+ const useStyles = ({ stacked }: { stacked: boolean }) => {
269
296
  const theme = useTheme()
270
- const atFontScaleBreakpoint = useAtFontScaleBreakpoint()
271
297
 
272
298
  return StyleSheet.create({
273
299
  listHeader: {
@@ -277,11 +303,11 @@ const useStyles = () => {
277
303
  gap: 8,
278
304
  },
279
305
  headingRow: {
280
- gap: 12,
281
- flexDirection: atFontScaleBreakpoint ? 'column' : 'row',
282
- alignItems: atFontScaleBreakpoint ? 'flex-start' : 'center',
306
+ gap: HEADING_ROW_GAP,
307
+ flexDirection: stacked ? 'column' : 'row',
308
+ alignItems: stacked ? 'flex-start' : 'center',
283
309
  justifyContent: 'space-between',
284
- paddingBottom: atFontScaleBreakpoint ? 4 : 0,
310
+ paddingBottom: stacked ? 4 : 0,
285
311
  paddingHorizontal: ROW_PADDING_HORIZONTAL,
286
312
  },
287
313
  titleContainer: {
@@ -8,7 +8,6 @@ import { analyticsEvents, usePublishProductAnalyticsEvent } from '../../hooks/us
8
8
  import { useSearchEnabled } from '../../hooks/use_search_enabled'
9
9
  import { AppName } from '../../types/resources/app_name'
10
10
  import { GraphId } from '../../types/resources/group_resource'
11
- import { Menu } from '../../utils/native_adapters'
12
11
  import { FilterDropdownListHeaderComponent } from './components/filter_dropdown_list_header_component'
13
12
  import { ListHeaderComponent } from './components/list_header_component'
14
13
  import { SearchListHeaderComponent } from './components/search_list_header_component'
@@ -41,7 +40,7 @@ const useListHeaderComponent = () => {
41
40
  const filterDropdownEnabled = useFilterDropdownEnabled()
42
41
  const searchEnabled = useSearchEnabled()
43
42
 
44
- if (filterDropdownEnabled && Menu.isConfigured) return FilterDropdownListHeaderComponent
43
+ if (filterDropdownEnabled) return FilterDropdownListHeaderComponent
45
44
  if (searchEnabled) return SearchListHeaderComponent
46
45
 
47
46
  return ListHeaderComponent
@@ -20,6 +20,7 @@ import {
20
20
  IconButton,
21
21
  Image,
22
22
  Person,
23
+ PopoverMenuView,
23
24
  Spinner,
24
25
  Switch,
25
26
  Text,
@@ -223,6 +224,7 @@ function PressablesSection({ isLast }: SectionProps) {
223
224
  const styles = useStyles()
224
225
  const [filterSelected, setFilterSelected] = useState('Groups')
225
226
  const [dropdownSelected, setDropdownSelected] = useState('all')
227
+ const [popoverSelected, setPopoverSelected] = useState('all')
226
228
 
227
229
  return (
228
230
  <CollapsableSection title="Pressables" isLast={isLast}>
@@ -400,7 +402,7 @@ function PressablesSection({ isLast }: SectionProps) {
400
402
  </Group>
401
403
  <Group
402
404
  title="FilterDropdown"
403
- description="Trigger that opens the host's native menu of filter options plus a More filters action. Needs a menu adapter."
405
+ description="Trigger that opens the host's native menu of filter options plus a More filters action. Falls back to PopoverMenuView when the host configures no menu adapter."
404
406
  >
405
407
  <Row style={styles.alignRowLeft}>
406
408
  <FilterDropdown
@@ -414,6 +416,37 @@ function PressablesSection({ isLast }: SectionProps) {
414
416
  />
415
417
  </Row>
416
418
  </Group>
419
+ <Group
420
+ title="PopoverMenuView"
421
+ description="The JS-only menu FilterDropdown falls back to when the host configures no menu adapter. Opens under the trigger, or above it when the trigger is in the lower half of the screen."
422
+ >
423
+ <Row style={styles.alignRowLeft}>
424
+ <PopoverMenuView
425
+ actions={[
426
+ ...filterDropdownOptions.map(option => ({
427
+ id: option.id,
428
+ title: option.label,
429
+ state: option.id === popoverSelected ? ('on' as const) : ('off' as const),
430
+ })),
431
+ {
432
+ id: 'more',
433
+ title: '',
434
+ displayInline: true,
435
+ subactions: [{ id: 'more_filters', title: 'More filters' }],
436
+ },
437
+ ]}
438
+ onPressAction={({ nativeEvent }) =>
439
+ nativeEvent.event === 'more_filters'
440
+ ? Alert.alert('More filters')
441
+ : setPopoverSelected(nativeEvent.event)
442
+ }
443
+ >
444
+ <Text>
445
+ {filterDropdownOptions.find(option => option.id === popoverSelected)?.label} ▾
446
+ </Text>
447
+ </PopoverMenuView>
448
+ </Row>
449
+ </Group>
417
450
  <Group
418
451
  title="IconButton"
419
452
  description="Supports different appearances, sizes, along with loading & disabled states. Use `iconStyle` for custom colors and font sizes. Requires `accessibilityLabel` as icon's don't provide context to screen readers."
@@ -1,5 +1,5 @@
1
- import React, { useEffect } from 'react'
2
- import { StyleProp, View, ViewStyle } from 'react-native'
1
+ import React from 'react'
2
+ import { StyleProp, ViewStyle } from 'react-native'
3
3
 
4
4
  // =================================
5
5
  // ====== Types ====================
@@ -47,24 +47,10 @@ export type MenuView = React.ComponentType<MenuViewProps>
47
47
  // ====== Adapter ==================
48
48
  // =================================
49
49
 
50
- function UnconfiguredMenuView({ children, style, testID }: MenuViewProps) {
51
- useEffect(() => {
52
- console.warn('ChatAdapters.configure() must include a menu adapter before rendering a menu')
53
- }, [])
54
-
55
- return (
56
- <View style={style} testID={testID}>
57
- {children}
58
- </View>
59
- )
60
- }
61
-
62
50
  export class MenuAdapter {
63
- View: MenuView
64
- isConfigured: boolean
51
+ View?: MenuView
65
52
 
66
53
  constructor(methods?: { View: MenuView }) {
67
- this.View = methods?.View ?? UnconfiguredMenuView
68
- this.isConfigured = !!methods?.View
54
+ this.View = methods?.View
69
55
  }
70
56
  }
@@ -1,46 +0,0 @@
1
- import { render, screen } from '@testing-library/react-native'
2
- import React from 'react'
3
- import { Text } from 'react-native'
4
- import { MenuAdapter, MenuViewProps } from '../menu'
5
-
6
- const actions = [{ id: 'all', title: 'All' }]
7
-
8
- describe('MenuAdapter', () => {
9
- it('renders the trigger and warns when the host configured no menu', () => {
10
- const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
11
- const { View, isConfigured } = new MenuAdapter()
12
-
13
- expect(isConfigured).toBe(false)
14
-
15
- render(
16
- <View actions={actions}>
17
- <Text>Trigger</Text>
18
- </View>
19
- )
20
-
21
- expect(screen.getByText('Trigger')).toBeTruthy()
22
- expect(warn).toHaveBeenCalledTimes(1)
23
- warn.mockRestore()
24
- })
25
-
26
- it('renders through the view the host supplies', () => {
27
- const HostMenuView = ({ actions: hostActions, children }: MenuViewProps) => (
28
- <>
29
- {children}
30
- <Text>{hostActions.map(action => action.title).join(',')}</Text>
31
- </>
32
- )
33
- const { View, isConfigured } = new MenuAdapter({ View: HostMenuView })
34
-
35
- expect(isConfigured).toBe(true)
36
-
37
- render(
38
- <View actions={actions}>
39
- <Text>Trigger</Text>
40
- </View>
41
- )
42
-
43
- expect(screen.getByText('Trigger')).toBeTruthy()
44
- expect(screen.getByText('All')).toBeTruthy()
45
- })
46
- })