-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
refactor: modernize Divider to latest MD3 spec #5089
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2d89666
de7af32
1dd5899
3650a28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,67 @@ | ||
| import { FlatList } from 'react-native'; | ||
| import * as React from 'react'; | ||
| import { StyleSheet, View } from 'react-native'; | ||
|
|
||
| import { Divider, List, useTheme } from 'react-native-paper'; | ||
| import { Divider, List, Text } from 'react-native-paper'; | ||
|
|
||
| import ScreenWrapper from '../ScreenWrapper'; | ||
|
|
||
| const items = ['Apple', 'Banana', 'Coconut', 'Lemon', 'Mango', 'Peach']; | ||
| const items = ['Apple', 'Banana', 'Coconut']; | ||
|
|
||
| const DividerExample = () => { | ||
| const { colors } = useTheme(); | ||
|
|
||
| return ( | ||
| <ScreenWrapper withScrollView={false}> | ||
| <FlatList | ||
| style={{ backgroundColor: colors?.background }} | ||
| renderItem={({ item }) => <List.Item title={item} />} | ||
| keyExtractor={(item) => item} | ||
| ItemSeparatorComponent={Divider} | ||
| data={items} | ||
| alwaysBounceVertical={false} | ||
| /> | ||
| <ScreenWrapper> | ||
| <List.Section title="Full width"> | ||
| {items.map((item) => ( | ||
| <React.Fragment key={item}> | ||
| <List.Item title={item} /> | ||
| <Divider /> | ||
| </React.Fragment> | ||
| ))} | ||
| </List.Section> | ||
| <List.Section title="Inset from the start"> | ||
| {items.map((item) => ( | ||
| <React.Fragment key={item}> | ||
| <List.Item title={item} /> | ||
| <Divider startInset /> | ||
| </React.Fragment> | ||
| ))} | ||
| </List.Section> | ||
| <List.Section title="Inset from both sides"> | ||
| {items.map((item) => ( | ||
| <React.Fragment key={item}> | ||
| <List.Item title={item} /> | ||
| <Divider horizontalInset /> | ||
| </React.Fragment> | ||
| ))} | ||
| </List.Section> | ||
| <List.Section title="Vertical"> | ||
| <View style={styles.row}> | ||
| {items.map((item, index) => ( | ||
| <React.Fragment key={item}> | ||
| {index > 0 && <Divider orientation="vertical" horizontalInset />} | ||
| <Text variant="bodyLarge" style={styles.column}> | ||
| {item} | ||
| </Text> | ||
| </React.Fragment> | ||
| ))} | ||
| </View> | ||
| </List.Section> | ||
| </ScreenWrapper> | ||
| ); | ||
| }; | ||
|
|
||
| DividerExample.title = 'Divider'; | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| row: { | ||
| flexDirection: 'row', | ||
| marginHorizontal: 16, | ||
| }, | ||
| column: { | ||
| flex: 1, | ||
| paddingVertical: 24, | ||
| textAlign: 'center', | ||
| }, | ||
| }); | ||
|
|
||
| export default DividerExample; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,22 +5,25 @@ import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; | |
| import { useInternalTheme } from '../core/theming'; | ||
| import type { ThemeProp } from '../theme/types'; | ||
|
|
||
| const THICKNESS = 1; | ||
| const INSET = 16; | ||
|
|
||
| export type Props = Omit<React.PropsWithoutRef<ViewProps>, 'children'> & { | ||
| /** | ||
| * @renamed Renamed from 'inset' to 'leftInset` in v5.x | ||
| * Whether divider has a left inset. | ||
| * Orientation of the divider. A vertical divider stretches to the height of | ||
| * its parent, so the parent has to lay its children out in a row. | ||
| */ | ||
| leftInset?: boolean; | ||
| orientation?: 'horizontal' | 'vertical'; | ||
| /** | ||
| * @supported Available in v5.x with theme version 3 | ||
| * Whether divider has a horizontal inset on both sides. | ||
| * Whether the divider is inset from the leading edge, which is the left edge | ||
| * in LTR and the right edge in RTL. On a vertical divider it's the top edge. | ||
| */ | ||
| horizontalInset?: boolean; | ||
| startInset?: boolean; | ||
| /** | ||
| * @supported Available in v5.x with theme version 3 | ||
| * Whether divider should be bolded. | ||
| * Whether the divider is inset from both edges: left and right on a | ||
| * horizontal divider, top and bottom on a vertical one. | ||
| */ | ||
| bold?: boolean; | ||
| horizontalInset?: boolean; | ||
| style?: StyleProp<ViewStyle>; | ||
| /** | ||
| * @optional | ||
|
|
@@ -31,6 +34,10 @@ export type Props = Omit<React.PropsWithoutRef<ViewProps>, 'children'> & { | |
| /** | ||
| * A divider is a thin, lightweight separator that groups content in lists and page layouts. | ||
| * | ||
| * Dividers are decorative, so screen readers skip them. If a divider means | ||
| * something on its own, pass `accessible`, `aria-hidden={false}` and | ||
| * `role="separator"`. | ||
| * | ||
| * ## Usage | ||
| * ```js | ||
| * import * as React from 'react'; | ||
|
|
@@ -50,41 +57,55 @@ export type Props = Omit<React.PropsWithoutRef<ViewProps>, 'children'> & { | |
| * ``` | ||
| */ | ||
| const Divider = ({ | ||
| leftInset, | ||
| orientation = 'horizontal', | ||
| startInset = false, | ||
| horizontalInset = false, | ||
| style, | ||
| theme: themeOverrides, | ||
| bold = false, | ||
| ...rest | ||
| }: Props) => { | ||
| const theme = useInternalTheme(themeOverrides); | ||
|
|
||
| const dividerColor = theme.colors.outlineVariant; | ||
| const isVertical = orientation === 'vertical'; | ||
|
|
||
| return ( | ||
| <View | ||
| aria-hidden | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tests that look up a divider will stop finding it - There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I've added a note in the migration guide. Also fixed latest conflicts. |
||
| {...rest} | ||
| style={[ | ||
| { height: StyleSheet.hairlineWidth, backgroundColor: dividerColor }, | ||
| leftInset && styles.v3LeftInset, | ||
| horizontalInset && styles.horizontalInset, | ||
| bold && styles.bold, | ||
| isVertical ? styles.vertical : styles.horizontal, | ||
| { backgroundColor: theme.colors.outlineVariant }, | ||
| startInset && | ||
| (isVertical ? styles.verticalStartInset : styles.startInset), | ||
| horizontalInset && | ||
| (isVertical ? styles.verticalInset : styles.horizontalInset), | ||
| style, | ||
| ]} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| v3LeftInset: { | ||
| marginLeft: 16, | ||
| horizontal: { | ||
| height: THICKNESS, | ||
| }, | ||
| vertical: { | ||
| width: THICKNESS, | ||
| alignSelf: 'stretch', | ||
| }, | ||
| startInset: { | ||
| marginStart: INSET, | ||
| }, | ||
| horizontalInset: { | ||
| marginLeft: 16, | ||
| marginRight: 16, | ||
| marginStart: INSET, | ||
| marginEnd: INSET, | ||
| }, | ||
| verticalStartInset: { | ||
| marginTop: INSET, | ||
| }, | ||
| bold: { | ||
| height: 1, | ||
| verticalInset: { | ||
| marginTop: INSET, | ||
| marginBottom: INSET, | ||
| }, | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { expect, it } from '@jest/globals'; | ||
|
|
||
| import { defaultThemes } from '../../core/theming'; | ||
| import { render, screen } from '../../test-utils'; | ||
| import Divider from '../Divider'; | ||
|
|
||
| const hidden = { includeHiddenElements: true }; | ||
|
|
||
| it('renders divider', async () => { | ||
| const tree = (await render(<Divider />)).toJSON(); | ||
|
|
||
| expect(tree).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it('renders vertical divider', async () => { | ||
| const tree = (await render(<Divider orientation="vertical" />)).toJSON(); | ||
|
|
||
| expect(tree).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it('renders 1dp thick horizontal line by default', async () => { | ||
| await render(<Divider testID="divider" />); | ||
|
|
||
| expect(screen.getByTestId('divider', hidden)).toHaveStyle({ | ||
| height: 1, | ||
| backgroundColor: defaultThemes.light.colors.outlineVariant, | ||
| }); | ||
| }); | ||
|
|
||
| it('renders 1dp thick line stretched to the parent when vertical', async () => { | ||
| await render(<Divider orientation="vertical" testID="divider" />); | ||
|
|
||
| expect(screen.getByTestId('divider', hidden)).toHaveStyle({ | ||
| width: 1, | ||
| alignSelf: 'stretch', | ||
| backgroundColor: defaultThemes.light.colors.outlineVariant, | ||
| }); | ||
| expect(screen.getByTestId('divider', hidden)).not.toHaveStyle({ height: 1 }); | ||
| }); | ||
|
|
||
| it('insets the start edge in a writing direction aware way', async () => { | ||
| await render(<Divider startInset testID="divider" />); | ||
|
|
||
| const divider = screen.getByTestId('divider', hidden); | ||
|
|
||
| expect(divider).toHaveStyle({ marginStart: 16 }); | ||
| expect(divider).not.toHaveStyle({ marginLeft: 16 }); | ||
| }); | ||
|
|
||
| it('insets both edges', async () => { | ||
| await render(<Divider horizontalInset testID="divider" />); | ||
|
|
||
| const divider = screen.getByTestId('divider', hidden); | ||
|
|
||
| expect(divider).toHaveStyle({ marginStart: 16, marginEnd: 16 }); | ||
| expect(divider).not.toHaveStyle({ marginLeft: 16 }); | ||
| expect(divider).not.toHaveStyle({ marginRight: 16 }); | ||
| }); | ||
|
|
||
| it('insets the leading end of a vertical divider', async () => { | ||
| await render(<Divider orientation="vertical" startInset testID="divider" />); | ||
|
|
||
| const divider = screen.getByTestId('divider', hidden); | ||
|
|
||
| expect(divider).toHaveStyle({ marginTop: 16 }); | ||
| expect(divider).not.toHaveStyle({ marginStart: 16 }); | ||
| }); | ||
|
|
||
| it('insets both ends of a vertical divider', async () => { | ||
| await render( | ||
| <Divider orientation="vertical" horizontalInset testID="divider" /> | ||
| ); | ||
|
|
||
| expect(screen.getByTestId('divider', hidden)).toHaveStyle({ | ||
| marginTop: 16, | ||
| marginBottom: 16, | ||
| }); | ||
| }); | ||
|
|
||
| it('applies custom styles over the defaults', async () => { | ||
| await render(<Divider style={{ height: 4 }} testID="divider" />); | ||
|
|
||
| expect(screen.getByTestId('divider', hidden)).toHaveStyle({ height: 4 }); | ||
| }); | ||
|
|
||
| it('stays out of the accessibility tree', async () => { | ||
| await render(<Divider testID="divider" />); | ||
|
|
||
| expect(screen.queryByTestId('divider')).toBeNull(); | ||
| expect(screen.getByTestId('divider', hidden)).toHaveProp('aria-hidden', true); | ||
| }); | ||
|
|
||
| it('can be exposed as a separator', async () => { | ||
| await render(<Divider accessible aria-hidden={false} role="separator" />); | ||
|
|
||
| expect(screen.getByRole('separator')).toBeOnTheScreen(); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This conflicts with
mainnow - #5078 landed a## General changessection at the same insertion point. Both sides only add text, so keeping both resolves it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebased on main and resolved the conflicts: