Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

Commit ae9b5a3

Browse files
committed
Prototype: Context usage for collections
1 parent b407e90 commit ae9b5a3

File tree

8 files changed

+656
-4
lines changed

8 files changed

+656
-4
lines changed

docs/src/examples/components/List/Performance/ListCommon.perf.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { List, Image } from '@fluentui/react'
1+
import { ContextList as List, Image } from '@fluentui/react'
22
import * as React from 'react'
33

44
const avatars = {

docs/src/examples/components/List/Types/ListExampleSelectable.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,18 @@ const ListExampleSelectable = () => (
88
header="Irving Kuhic"
99
headerMedia="7:26:56 AM"
1010
content="Program the sensor to the SAS alarm through the haptic SQL card!"
11-
selectable
1211
/>
1312
<List.Item
1413
media={<Image src="public/images/avatar/small/steve.jpg" avatar />}
1514
header="Skyler Parks"
1615
headerMedia="11:30:17 PM"
1716
content="Use the online FTP application to input the multi-byte application!"
18-
selectable
1917
/>
2018
<List.Item
2119
media={<Image src="public/images/avatar/small/nom.jpg" avatar />}
2220
header="Dante Schneider"
2321
headerMedia="5:22:40 PM"
2422
content="The GB pixel is down, navigate the virtual interface!"
25-
selectable
2623
/>
2724
</List>
2825
)
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { Accessibility, listBehavior } from '@fluentui/accessibility'
2+
import * as customPropTypes from '@fluentui/react-proptypes'
3+
import * as _ from 'lodash'
4+
import * as React from 'react'
5+
import * as PropTypes from 'prop-types'
6+
7+
import {
8+
childrenExist,
9+
UIComponentProps,
10+
ChildrenComponentProps,
11+
commonPropTypes,
12+
rtlTextContainer,
13+
} from '../../utils'
14+
import ListItem, { ListItemProps } from './ListItem'
15+
import {
16+
WithAsProp,
17+
ComponentEventHandler,
18+
withSafeTypeForAs,
19+
ShorthandCollection,
20+
} from '../../types'
21+
import { createManager, ManagerFactory } from '@fluentui/state'
22+
import {
23+
getElementType,
24+
getUnhandledProps,
25+
useAccessibility,
26+
useStateManager,
27+
} from '@fluentui/react-bindings'
28+
import useStyles from './useStyles'
29+
import { useListProvider } from './ListContext'
30+
31+
export interface ListSlotClassNames {
32+
item: string
33+
}
34+
35+
export interface ListProps extends UIComponentProps, ChildrenComponentProps {
36+
/** Accessibility behavior if overridden by the user. */
37+
accessibility?: Accessibility
38+
39+
/** Toggle debug mode */
40+
debug?: boolean
41+
42+
/** Shorthand array of props for ListItem. */
43+
items?: ShorthandCollection<ListItemProps>
44+
45+
/** A selectable list formats list items as possible choices. */
46+
selectable?: boolean
47+
48+
/** A navigable list allows user to navigate through items. */
49+
navigable?: boolean
50+
51+
/** Index of the currently selected item. */
52+
selectedIndex?: number
53+
54+
/** Initial selectedIndex value. */
55+
defaultSelectedIndex?: number
56+
57+
/**
58+
* Event for request to change 'selectedIndex' value.
59+
* @param event - React's original SyntheticEvent.
60+
* @param data - All props and proposed value.
61+
*/
62+
onSelectedIndexChange?: ComponentEventHandler<ListProps>
63+
64+
/** Truncates content */
65+
truncateContent?: boolean
66+
67+
/** Truncates header */
68+
truncateHeader?: boolean
69+
70+
/** A horizontal list displays elements horizontally. */
71+
horizontal?: boolean
72+
}
73+
74+
export interface ListState {
75+
selectedIndex?: number
76+
}
77+
78+
type ListActions = {
79+
select: (index: number) => void
80+
}
81+
82+
type ListComponent = React.FC<WithAsProp<ListProps>> & {
83+
className: string
84+
slotClassNames: ListSlotClassNames
85+
86+
Item: typeof ListItem
87+
}
88+
89+
const createListManager: ManagerFactory<ListState, ListActions> = config =>
90+
createManager<ListState, ListActions>({
91+
...config,
92+
actions: {
93+
select: index => () => ({ selectedIndex: index }),
94+
},
95+
state: {
96+
selectedIndex: -1,
97+
...config.state,
98+
},
99+
})
100+
101+
const List: ListComponent = props => {
102+
const { children, selectable, navigable, horizontal, items } = props
103+
104+
const ElementType = getElementType(props)
105+
const unhandledProps = getUnhandledProps(Object.keys(List.propTypes) as any, props)
106+
107+
const [state, actions] = useStateManager(createListManager, {
108+
mapPropsToInitialState: () => ({
109+
selectedIndex: props.defaultSelectedIndex,
110+
}),
111+
mapPropsToState: () => ({
112+
selectedIndex: props.selectedIndex,
113+
}),
114+
})
115+
const getA11Props = useAccessibility(props.accessibility, {
116+
debugName: List.displayName,
117+
mapPropsToBehavior: () => ({
118+
selectable,
119+
navigable,
120+
horizontal,
121+
}),
122+
})
123+
const [classes] = useStyles(List.displayName, {
124+
className: List.className,
125+
mapPropsToStyles: () => props,
126+
})
127+
128+
const [Provider, value] = useListProvider({
129+
debug: props.debug,
130+
selectable: props.selectable,
131+
navigable: props.navigable,
132+
truncateContent: props.truncateContent,
133+
truncateHeader: props.truncateHeader,
134+
variables: props.variables,
135+
136+
onItemClick: (e, index) => {
137+
if (selectable) {
138+
actions.select(index)
139+
_.invoke(props, 'onSelectedIndexChange', e, {
140+
...props,
141+
selectedIndex: index,
142+
})
143+
}
144+
},
145+
selectedIndex: state.selectedIndex,
146+
})
147+
148+
return (
149+
<ElementType
150+
{...getA11Props('root', {
151+
className: classes.root,
152+
...rtlTextContainer.getAttributes({ forElements: [children] }),
153+
...unhandledProps,
154+
})}
155+
>
156+
<Provider value={value}>
157+
{childrenExist(children) ? children : _.map(items, ListItem.create)}
158+
</Provider>
159+
</ElementType>
160+
)
161+
}
162+
163+
List.displayName = 'List'
164+
List.className = 'ui-list'
165+
List.slotClassNames = {
166+
item: `${List.className}__item`,
167+
}
168+
List.propTypes = {
169+
...commonPropTypes.createCommon({
170+
content: false,
171+
}),
172+
debug: PropTypes.bool,
173+
items: customPropTypes.collectionShorthand,
174+
selectable: customPropTypes.every([customPropTypes.disallow(['navigable']), PropTypes.bool]),
175+
navigable: customPropTypes.every([customPropTypes.disallow(['selectable']), PropTypes.bool]),
176+
truncateContent: PropTypes.bool,
177+
truncateHeader: PropTypes.bool,
178+
selectedIndex: PropTypes.number,
179+
defaultSelectedIndex: PropTypes.number,
180+
onSelectedIndexChange: PropTypes.func,
181+
horizontal: PropTypes.bool,
182+
} as any
183+
List.defaultProps = {
184+
as: 'ul',
185+
accessibility: listBehavior as Accessibility,
186+
}
187+
188+
List.Item = ListItem
189+
190+
/**
191+
* A List displays a group of related sequential items.
192+
*
193+
* @accessibility
194+
* List may follow one of the following accessibility semantics:
195+
* - Static non-navigable list. Implements [ARIA list](https://www.w3.org/TR/wai-aria-1.1/#list) role.
196+
* - Selectable list: allows the user to select item from a list of choices. Implements [ARIA Listbox](https://www.w3.org/TR/wai-aria-practices-1.1/#Listbox) design pattern.
197+
*/
198+
export default withSafeTypeForAs<typeof List, ListProps, 'ul'>(List)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as React from 'react'
2+
import { createContext, useContextSelector } from './context'
3+
import { ListItemProps } from '@fluentui/react'
4+
5+
type ListContextValue = {
6+
debug: ListItemProps['variables']
7+
selectable: ListItemProps['selectable']
8+
navigable: ListItemProps['navigable']
9+
truncateContent: ListItemProps['truncateContent']
10+
truncateHeader: ListItemProps['truncateHeader']
11+
variables: ListItemProps['variables']
12+
13+
onItemClick: (index: number) => void
14+
selectedIndex: number
15+
}
16+
17+
const ListContext = createContext<ListContextValue>(null)
18+
19+
export const useListProvider = props => {
20+
const registeredItems = React.useRef([])
21+
22+
const registerChild = React.useCallback(child => {
23+
let index = registeredItems.current.indexOf(child)
24+
25+
if (index === -1) {
26+
index = registeredItems.current.push(child) - 1
27+
}
28+
29+
return index
30+
}, [])
31+
32+
const unregisterChild = React.useCallback(child => {
33+
const index = registeredItems.current.indexOf(child)
34+
35+
registeredItems.current.splice(index, -1)
36+
}, [])
37+
38+
const value = {
39+
...props,
40+
registerChild,
41+
unregisterChild,
42+
}
43+
44+
return [ListContext.Provider, value]
45+
}
46+
47+
export const useListConsumer = () => {
48+
const ref = React.useRef(null)
49+
50+
const registerChild = useContextSelector(ListContext, v => v.registerChild)
51+
const unregisterChild = useContextSelector(ListContext, v => v.unregisterChild)
52+
53+
const currentIndex = registerChild(ref)
54+
55+
React.useEffect(() => {
56+
return () => {
57+
unregisterChild(ref)
58+
}
59+
}, [unregisterChild])
60+
61+
const selected = useContextSelector(ListContext, v => v.selectedIndex === currentIndex)
62+
const onClick = useContextSelector(ListContext, v => v.onItemClick)
63+
const debug = useContextSelector(ListContext, v => v.debug)
64+
const selectable = useContextSelector(ListContext, v => v.selectable)
65+
const navigable = useContextSelector(ListContext, v => v.navigable)
66+
const truncateContent = useContextSelector(ListContext, v => v.truncateContent)
67+
const truncateHeader = useContextSelector(ListContext, v => v.truncateHeader)
68+
const variables = useContextSelector(ListContext, v => v.variables)
69+
70+
const selectedIndex = useContextSelector(ListContext, v => v.selectedIndex)
71+
72+
return {
73+
selected,
74+
onClick: e => {
75+
onClick(e, currentIndex)
76+
},
77+
debug,
78+
selectable,
79+
navigable,
80+
truncateContent,
81+
truncateHeader,
82+
variables,
83+
selectedIndex,
84+
}
85+
}

0 commit comments

Comments
 (0)