@planningcenter/chat-react-native 4.5.0-rc.2 → 4.5.0-rc.4

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 +16 -0
  2. package/build/components/display/filter_dropdown.d.ts.map +1 -1
  3. package/build/components/display/filter_dropdown.js +105 -57
  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 +159 -124
  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 +53 -19
  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 +0 -54
  34. package/src/screens/conversations/components/filter_dropdown_list_header_component.tsx +41 -27
  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,17 @@
1
1
  import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
2
2
  import React, { useCallback, useMemo, useState } from 'react'
3
3
  import { Alert, LayoutChangeEvent, StyleSheet, View } from 'react-native'
4
- import { FilterDropdown, FilterDropdownOption, TextButton } from '../../../components'
4
+ import {
5
+ FilterDropdown,
6
+ FilterDropdownOption,
7
+ FilterDropdownTrigger,
8
+ TextButton,
9
+ } from '../../../components'
5
10
  import {
6
11
  useCanBrowseArchivedConversations,
7
12
  useCanCreateConversations,
8
13
  useCurrentPersonCache,
9
14
  useDirectMessagesEligible,
10
- useFontScale,
11
15
  useHasDirectMessages,
12
16
  useNewConversationEntry,
13
17
  useTheme,
@@ -26,7 +30,6 @@ import { ListHeaderActionButton } from './list_header_action_button'
26
30
 
27
31
  const ROW_PADDING_HORIZONTAL = 16
28
32
  const HEADING_ROW_GAP = 12
29
- const DEFAULT_FONT_SCALE = 1
30
33
  const ALL_CONVERSATIONS_LABEL = 'All conversations'
31
34
  const ARCHIVED_LABEL = 'Archived'
32
35
 
@@ -207,31 +210,43 @@ export const FilterDropdownListHeaderComponent = () => {
207
210
 
208
211
  const showMoreFilters = canFilterByGroups || canFilterByTeams || canBrowseArchived
209
212
 
210
- const stacked = useFontScale() > DEFAULT_FONT_SCALE
211
- const styles = useStyles({ stacked })
213
+ const styles = useStyles()
212
214
 
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
+ // Cap the trigger beside the actions while the widest fixed label fits there, so only longer
216
+ // labels truncate; past that, cap it to the row and let the actions wrap.
215
217
  const [rowWidth, setRowWidth] = useState<number>()
216
218
  const [actionsWidth, setActionsWidth] = useState<number>()
219
+ const [widestFixedLabelWidth, setWidestFixedLabelWidth] = useState<number>()
217
220
  const handleRowLayout = useCallback((event: LayoutChangeEvent) => {
218
221
  setRowWidth(event.nativeEvent.layout.width)
219
222
  }, [])
220
223
  const handleActionsLayout = useCallback((event: LayoutChangeEvent) => {
221
224
  setActionsWidth(event.nativeEvent.layout.width)
222
225
  }, [])
223
- const triggerMaxWidth =
224
- !stacked && rowWidth !== undefined && actionsWidth !== undefined
225
- ? rowWidth - ROW_PADDING_HORIZONTAL * 2 - actionsWidth - HEADING_ROW_GAP
226
- : undefined
226
+ const handleWidestFixedLabelLayout = useCallback((event: LayoutChangeEvent) => {
227
+ setWidestFixedLabelWidth(event.nativeEvent.layout.width)
228
+ }, [])
229
+ const triggerMaxWidth = useMemo(() => {
230
+ if (rowWidth === undefined) return undefined
231
+
232
+ const rowContentWidth = rowWidth - ROW_PADDING_HORIZONTAL * 2
233
+ if (actionsWidth === undefined || widestFixedLabelWidth === undefined) return rowContentWidth
234
+
235
+ const roomBesideActions = rowContentWidth - actionsWidth - HEADING_ROW_GAP
236
+ return roomBesideActions >= widestFixedLabelWidth ? roomBesideActions : rowContentWidth
237
+ }, [rowWidth, actionsWidth, widestFixedLabelWidth])
227
238
 
228
239
  return (
229
240
  <View style={styles.listHeader}>
230
- <View
231
- style={styles.headingRow}
232
- onLayout={handleRowLayout}
233
- testID="conversations-filter-heading-row"
234
- >
241
+ <View style={styles.widestFixedLabel} pointerEvents="none">
242
+ <FilterDropdownTrigger
243
+ label={ALL_CONVERSATIONS_LABEL}
244
+ onLayout={handleWidestFixedLabelLayout}
245
+ accessibilityElementsHidden
246
+ importantForAccessibility="no-hide-descendants"
247
+ />
248
+ </View>
249
+ <View style={styles.headingRow} onLayout={handleRowLayout}>
235
250
  <View style={styles.titleContainer}>
236
251
  <FilterDropdown
237
252
  options={options}
@@ -243,11 +258,7 @@ export const FilterDropdownListHeaderComponent = () => {
243
258
  maxWidth={triggerMaxWidth}
244
259
  />
245
260
  </View>
246
- <View
247
- style={styles.actionsContainer}
248
- onLayout={handleActionsLayout}
249
- testID="conversations-filter-actions"
250
- >
261
+ <View style={styles.actionsContainer} onLayout={handleActionsLayout}>
251
262
  <ListHeaderActionButton
252
263
  name="general.cog"
253
264
  onPress={handleChatSettings}
@@ -292,7 +303,7 @@ export const FilterDropdownListHeaderComponent = () => {
292
303
  )
293
304
  }
294
305
 
295
- const useStyles = ({ stacked }: { stacked: boolean }) => {
306
+ const useStyles = () => {
296
307
  const theme = useTheme()
297
308
 
298
309
  return StyleSheet.create({
@@ -304,15 +315,18 @@ const useStyles = ({ stacked }: { stacked: boolean }) => {
304
315
  },
305
316
  headingRow: {
306
317
  gap: HEADING_ROW_GAP,
307
- flexDirection: stacked ? 'column' : 'row',
308
- alignItems: stacked ? 'flex-start' : 'center',
318
+ flexDirection: 'row',
319
+ flexWrap: 'wrap',
320
+ alignItems: 'center',
309
321
  justifyContent: 'space-between',
310
- paddingBottom: stacked ? 4 : 0,
311
322
  paddingHorizontal: ROW_PADDING_HORIZONTAL,
312
323
  },
313
324
  titleContainer: {
314
- flexShrink: 1,
315
- minWidth: 0,
325
+ flexShrink: 0,
326
+ },
327
+ widestFixedLabel: {
328
+ position: 'absolute',
329
+ opacity: 0,
316
330
  },
317
331
  actionsContainer: {
318
332
  flexDirection: 'row',
@@ -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
- })