diff --git a/docs/6.x/docs/components/Drawer/DrawerItem.mdx b/docs/6.x/docs/components/Drawer/DrawerItem.mdx index 4be921a561..1c4887013d 100644 --- a/docs/6.x/docs/components/Drawer/DrawerItem.mdx +++ b/docs/6.x/docs/components/Drawer/DrawerItem.mdx @@ -141,7 +141,7 @@ export default MyComponent; ## Theme colors - + diff --git a/docs/6.x/docs/components/Drawer/DrawerSection.mdx b/docs/6.x/docs/components/Drawer/DrawerSection.mdx index 5c34c9aef3..3c1a1d8a55 100644 --- a/docs/6.x/docs/components/Drawer/DrawerSection.mdx +++ b/docs/6.x/docs/components/Drawer/DrawerSection.mdx @@ -104,7 +104,7 @@ export default MyComponent; ## Theme colors - + diff --git a/docs/6.x/docs/guides/migration.md b/docs/6.x/docs/guides/migration.md index 1fcd32bd25..6e80a22fce 100644 --- a/docs/6.x/docs/guides/migration.md +++ b/docs/6.x/docs/guides/migration.md @@ -6,6 +6,23 @@ TBC ## Components +### Drawer + +The `Drawer` API is unchanged in Paper 6.x — no props were renamed, removed, or given new defaults. `Drawer.Item` and `Drawer.Section` were realigned with the current Material Design 3 navigation drawer spec, so their appearance changes: + +- **Trailing padding.** `Drawer.Item` reserved 36dp on the trailing edge and 28dp on the leading edge. Both are now 28dp, as the spec requires. Items sit 8dp wider. +- **Active label weight.** The label of an active `Drawer.Item` now uses the prominent `labelLargeEmphasized` weight (700). Inactive labels are unchanged at 500. +- **Section divider colour.** `Drawer.Section` drew its divider in a fixed grey that ignored the theme and was wrong in dark mode. It now uses `theme.colors.outlineVariant`, like every other `Divider`. +- **Focus indicator.** `Drawer.Item` gains a keyboard focus ring in `theme.colors.secondary`. It appears only on keyboard focus, not on press. + +If you were pixel-matching a drawer against Paper 5.x, the first two points are the ones that will shift your layout. To restore the old trailing padding, pass it through `style`: + +```tsx + +``` + +The theme also gains the 15 `*Emphasized` typescale keys (`labelLargeEmphasized`, `titleSmallEmphasized`, and so on). The values already existed in the token set; only the types were missing, so `` now typechecks. This is additive — no existing key changed. + ### TextInput The Paper 6.x `TextInput` is a complete rewrite with a new API. Import the component the same way, but note that the props and behavior have changed significantly. diff --git a/docs/public/screenshots/drawer-item.png b/docs/public/screenshots/drawer-item.png index 811aef04e7..de7ebc79f5 100644 Binary files a/docs/public/screenshots/drawer-item.png and b/docs/public/screenshots/drawer-item.png differ diff --git a/docs/public/screenshots/drawer-section.png b/docs/public/screenshots/drawer-section.png index a309aab159..d35b9053af 100644 Binary files a/docs/public/screenshots/drawer-section.png and b/docs/public/screenshots/drawer-section.png differ diff --git a/docs/src/data/componentDocs6x.json b/docs/src/data/componentDocs6x.json index 56b356912b..79beb86f8b 100644 --- a/docs/src/data/componentDocs6x.json +++ b/docs/src/data/componentDocs6x.json @@ -5646,7 +5646,7 @@ "tsType": { "name": "boolean" }, - "description": "Whether the item is disabled." + "description": "Whether the item is disabled. Disabled items are dimmed and don't respond to touch." }, "focusedIcon": { "required": false, @@ -5781,7 +5781,7 @@ "tsType": { "name": "boolean" }, - "description": "Whether the item is disabled." + "description": "Whether the item is disabled. Disabled items are dimmed and don't respond to touch." }, "onPress": { "required": false, diff --git a/docs/src/data/themeColors.ts b/docs/src/data/themeColors.ts index 20f16962f7..cf01cadfa7 100644 --- a/docs/src/data/themeColors.ts +++ b/docs/src/data/themeColors.ts @@ -140,10 +140,15 @@ export const themeColors = { inactive: { 'iconColor/textColor': 'theme.colors.onSurfaceVariant', }, + focused: { + focusIndicatorColor: 'theme.colors.secondary', + 'iconColor/textColor (inactive)': 'theme.colors.onSurface', + }, }, 'Drawer.Section': { '-': { titleColor: 'theme.colors.onSurfaceVariant', + dividerColor: 'theme.colors.outlineVariant', }, }, FAB: { diff --git a/example/src/DrawerItems.tsx b/example/src/DrawerItems.tsx index 94afa3136c..a8527a7dcd 100644 --- a/example/src/DrawerItems.tsx +++ b/example/src/DrawerItems.tsx @@ -135,11 +135,10 @@ function DrawerItems() { return ( @@ -175,7 +174,7 @@ function DrawerItems() { {dynamicThemeSupported ? ( - + Use Dynamic Theme @@ -184,7 +183,7 @@ function DrawerItems() { ) : null} - + Dark Theme @@ -193,7 +192,7 @@ function DrawerItems() { - + RTL @@ -202,7 +201,7 @@ function DrawerItems() { - + Collapsed drawer * @@ -211,7 +210,7 @@ function DrawerItems() { - + Custom font * @@ -220,7 +219,7 @@ function DrawerItems() { - + {isIOS ? 'Highlight' : 'Ripple'} effect * @@ -271,10 +270,6 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - paddingVertical: 12, - paddingHorizontal: 16, - }, - v3Preference: { height: 56, paddingHorizontal: 28, }, diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx index 53132e299f..c0896bbafe 100644 --- a/example/src/ExampleList.tsx +++ b/example/src/ExampleList.tsx @@ -19,6 +19,7 @@ import ChipExample from './Examples/ChipExample'; import DataTableExample from './Examples/DataTableExample'; import DialogExample from './Examples/DialogExample'; import DividerExample from './Examples/DividerExample'; +import DrawerExample from './Examples/DrawerExample'; import FABExample from './Examples/FABExample'; import IconButtonExample from './Examples/IconButtonExample'; import IconExample from './Examples/IconExample'; @@ -64,6 +65,7 @@ export const mainExamples = { DataTable: DataTableExample, Dialog: DialogExample, Divider: DividerExample, + Drawer: DrawerExample, FAB: FABExample, IconButton: IconButtonExample, Icon: IconExample, diff --git a/example/src/Examples/DrawerExample.tsx b/example/src/Examples/DrawerExample.tsx new file mode 100644 index 0000000000..f443fbd3b6 --- /dev/null +++ b/example/src/Examples/DrawerExample.tsx @@ -0,0 +1,196 @@ +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; +import type { ColorValue } from 'react-native'; + +import { Badge, Drawer, Text, useTheme } from 'react-native-paper'; + +import ScreenWrapper from '../ScreenWrapper'; + +const DRAWER_WIDTH = 360; + +const Section = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => { + const theme = useTheme(); + + return ( + + + {title} + + + {children} + + + ); +}; + +const Destinations = () => { + const [active, setActive] = React.useState('inbox'); + + return ( + + setActive('inbox')} + right={() => 24} + /> + setActive('starred')} + /> + setActive('sent')} + /> + + ); +}; + +const DrawerExample = () => { + const [collapsed, setCollapsed] = React.useState('inbox'); + + return ( + +
+ +
+ +
+ + + + +
+ +
+ + + 24} + /> + +
+ +
+ + } + /> + ( + + )} + /> + +
+ +
+ + + + + + +
+ +
+ + setCollapsed('inbox')} + /> + setCollapsed('starred')} + /> + setCollapsed('alerts')} + /> + + +
+
+ ); +}; + +DrawerExample.title = 'Drawer'; + +const styles = StyleSheet.create({ + content: { + paddingVertical: 16, + }, + section: { + marginBottom: 24, + }, + caption: { + marginBottom: 8, + marginHorizontal: 16, + }, + drawer: { + width: DRAWER_WIDTH, + maxWidth: '100%', + alignSelf: 'center', + paddingVertical: 8, + borderTopEndRadius: 16, + borderBottomEndRadius: 16, + }, + badge: { + alignSelf: 'center', + }, + collapsed: { + flexDirection: 'row', + justifyContent: 'center', + paddingVertical: 8, + }, +}); + +export default DrawerExample; diff --git a/src/components/Drawer/DrawerCollapsedItem.tsx b/src/components/Drawer/DrawerCollapsedItem.tsx index 6e4fdebaa6..2f0830ccb2 100644 --- a/src/components/Drawer/DrawerCollapsedItem.tsx +++ b/src/components/Drawer/DrawerCollapsedItem.tsx @@ -1,5 +1,12 @@ import * as React from 'react'; -import { Animated, Platform, Pressable, StyleSheet, View } from 'react-native'; +import { + Animated, + Easing, + Platform, + Pressable, + StyleSheet, + View, +} from 'react-native'; import type { GestureResponderEvent, NativeSyntheticEvent, @@ -9,7 +16,9 @@ import type { ViewStyle, } from 'react-native'; +import { DrawerCollapsedItemTokens } from './tokens'; import { useInternalTheme } from '../../core/theming'; +import { resolveCornerRadius } from '../../theme/utils/shape'; import type { ThemeProp } from '../../types'; import Badge from '../Badge'; import Icon from '../Icon'; @@ -26,7 +35,7 @@ export type Props = ViewProps & { */ badge?: string | number | boolean; /** - * Whether the item is disabled. + * Whether the item is disabled. Disabled items are dimmed and don't respond to touch. */ disabled?: boolean; /** @@ -67,9 +76,12 @@ export type Props = ViewProps & { testID?: string; }; -const iconSize = 24; -const itemSize = 56; -const outlineHeight = 32; +const { + iconSize, + activeIndicatorWidth, + activeIndicatorHeight, + noLabelActiveIndicatorHeight, +} = DrawerCollapsedItemTokens; /** * Note: Available in v5.x with theme version 3 @@ -125,22 +137,36 @@ const DrawerCollapsedItem = ({ const handlePressOut = () => { Animated.timing(animScale, { toValue: 1, - duration: 150 * scale, + duration: + theme.motion.duration[ + DrawerCollapsedItemTokens.activeIndicatorDuration + ] * scale, + easing: Easing.bezier( + ...theme.motion.easing[DrawerCollapsedItemTokens.activeIndicatorEasing] + ), useNativeDriver: true, }).start(); }; - const iconPadding = ((!label ? itemSize : outlineHeight) - iconSize) / 2; + const iconPadding = + ((!label ? noLabelActiveIndicatorHeight : activeIndicatorHeight) - + iconSize) / + 2; + + const activeIndicatorRadius = resolveCornerRadius( + theme, + DrawerCollapsedItemTokens.activeIndicatorShape + ); const backgroundColor = active - ? theme.colors.secondaryContainer + ? theme.colors[DrawerCollapsedItemTokens.activeIndicatorColor] : 'transparent'; const labelColor = active - ? theme.colors.onSurface - : theme.colors.onSurfaceVariant; + ? theme.colors[DrawerCollapsedItemTokens.activeLabelTextColor] + : theme.colors[DrawerCollapsedItemTokens.inactiveLabelTextColor]; const iconColor = active - ? theme.colors.onSecondaryContainer - : theme.colors.onSurfaceVariant; + ? theme.colors[DrawerCollapsedItemTokens.activeIconColor] + : theme.colors[DrawerCollapsedItemTokens.inactiveIconColor]; const onTextLayout = ({ nativeEvent, @@ -155,18 +181,23 @@ const DrawerCollapsedItem = ({ const labelTextStyle = { color: labelColor, - ...theme.fonts.labelMedium, + ...theme.fonts[DrawerCollapsedItemTokens.labelText], }; const icon = !active && unfocusedIcon !== undefined ? unfocusedIcon : focusedIcon; + const opacity = disabled + ? DrawerCollapsedItemTokens.stateOpacity.disabled + : DrawerCollapsedItemTokens.stateOpacity.enabled; + return ( { const theme = useInternalTheme(themeOverrides); + const [focused, setFocused] = React.useState(false); - const backgroundColor = active ? theme.colors.secondaryContainer : undefined; + const handleFocus = (e: NativeSyntheticEvent) => { + if (isKeyboardFocusEvent(e)) { + setFocused(true); + } + }; + + const isFocused = Boolean(focused && !disabled); + + const backgroundColor = active + ? theme.colors[DrawerItemTokens.activeIndicatorColor] + : undefined; + const inactiveIconColor = isFocused + ? DrawerItemTokens.inactiveFocusIconColor + : DrawerItemTokens.inactiveIconColor; + const inactiveLabelTextColor = isFocused + ? DrawerItemTokens.inactiveFocusLabelTextColor + : DrawerItemTokens.inactiveLabelTextColor; const contentColor = active - ? theme.colors.onSecondaryContainer - : theme.colors.onSurfaceVariant; + ? theme.colors[DrawerItemTokens.activeIconColor] + : theme.colors[inactiveIconColor]; + const labelColor = active + ? theme.colors[DrawerItemTokens.activeLabelTextColor] + : theme.colors[inactiveLabelTextColor]; - const labelMargin = icon ? 12 : 0; - const borderRadius = theme.shapes.corner.extraLarge; - const font = theme.fonts.labelLarge; + const borderRadius = resolveCornerRadius( + theme, + DrawerItemTokens.indicatorShape + ); + const { inset } = DrawerItemTokens.focusIndicator; + const opacity = disabled + ? DrawerItemTokens.stateOpacity.disabled + : DrawerItemTokens.stateOpacity.enabled; return ( @@ -118,10 +148,12 @@ const DrawerItem = ({ disabled={disabled} background={background} onPress={onPress} + onFocus={handleFocus} + onBlur={() => setFocused(false)} style={[ styles.container, - styles.v3Container, - { backgroundColor, borderRadius }, + { backgroundColor, borderRadius, opacity }, + Platform.OS === 'web' ? webNoOutline : null, style, ]} role="button" @@ -130,30 +162,51 @@ const DrawerItem = ({ theme={theme} hitSlop={hitSlop} > - - - {icon ? ( - - ) : null} - + + + {icon ? ( + + ) : null} + + {label} + + + + {right?.({ color: contentColor })} + + + {isFocused ? ( + - {label} - - - - {right?.({ color: contentColor })} + /> + ) : null}
@@ -162,27 +215,22 @@ const DrawerItem = ({ DrawerItem.displayName = 'Drawer.Item'; +const webNoOutline = { outline: 'none' } as unknown as ViewStyle; + const styles = StyleSheet.create({ container: { - marginHorizontal: 10, - marginVertical: 4, + height: DrawerItemTokens.height, + justifyContent: 'center', + marginHorizontal: DrawerItemTokens.indicatorInset, }, - v3Container: { + inner: { + flex: 1, justifyContent: 'center', - height: 56, - marginLeft: 12, - marginRight: 12, - marginVertical: 0, }, wrapper: { flexDirection: 'row', alignItems: 'center', - padding: 8, - }, - v3Wrapper: { - marginLeft: 16, - marginRight: 24, - padding: 0, + marginHorizontal: DrawerItemTokens.contentInset, }, content: { flex: 1, @@ -190,7 +238,19 @@ const styles = StyleSheet.create({ alignItems: 'center', }, label: { - marginRight: 32, + marginEnd: DrawerItemTokens.labelTrailingGap, + }, + labelWithIcon: { + marginStart: DrawerItemTokens.iconLabelGap, + }, + focusRing: { + position: 'absolute', + top: DrawerItemTokens.focusIndicator.inset, + bottom: DrawerItemTokens.focusIndicator.inset, + left: DrawerItemTokens.focusIndicator.inset, + right: DrawerItemTokens.focusIndicator.inset, + borderWidth: DrawerItemTokens.focusIndicator.thickness, + pointerEvents: 'none', }, }); diff --git a/src/components/Drawer/DrawerSection.tsx b/src/components/Drawer/DrawerSection.tsx index d267cf4c17..b828a70d82 100644 --- a/src/components/Drawer/DrawerSection.tsx +++ b/src/components/Drawer/DrawerSection.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; +import { DrawerSectionTokens } from './tokens'; import { useInternalTheme } from '../../core/theming'; -import { Palette } from '../../theme/tokens'; import type { ThemeProp } from '../../types'; import Divider from '../Divider'; import Text from '../Typography/Text'; @@ -72,30 +72,22 @@ const DrawerSection = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); - const titleColor = theme.colors.onSurfaceVariant; - const titleMargin = 28; - const font = theme.fonts.titleSmall; return ( {title && ( - - {title && ( - - {title} - - )} + + + {title} + )} {children} @@ -103,8 +95,9 @@ const DrawerSection = ({ )} @@ -115,20 +108,17 @@ DrawerSection.displayName = 'Drawer.Section'; const styles = StyleSheet.create({ container: { - marginBottom: 4, + marginBottom: DrawerSectionTokens.bottomSpacing, }, titleContainer: { - height: 40, + height: DrawerSectionTokens.headlineHeight, justifyContent: 'center', }, - v3TitleContainer: { - height: 56, + title: { + marginStart: DrawerSectionTokens.headlinePadding, }, divider: { - marginTop: 4, - }, - v3Divider: { - backgroundColor: Palette.neutralVariant50, + marginTop: DrawerSectionTokens.dividerSpacing, }, }); diff --git a/src/components/Drawer/tokens.ts b/src/components/Drawer/tokens.ts new file mode 100644 index 0000000000..ac3eda86e8 --- /dev/null +++ b/src/components/Drawer/tokens.ts @@ -0,0 +1,123 @@ +import { tokens } from '../../theme/tokens'; +import type { + ColorRole, + MotionDuration, + MotionEasing, + TypescaleKey, +} from '../../theme/types'; +import type { ShapeToken } from '../../theme/utils/shape'; + +const stateOpacity = { + enabled: tokens.md.sys.state.opacity.enabled, + disabled: tokens.md.sys.state.opacity.disabled, +} as const; + +const itemSizes = { + height: 56, + iconSize: 24, + indicatorInset: 12, + contentInset: 16, + iconLabelGap: 12, + labelTrailingGap: 32, +} as const satisfies Record; + +const itemShape = { + indicatorShape: 'full', +} as const satisfies Record; + +const itemTypescale = { + labelText: 'labelLarge', + activeLabelText: 'labelLargeEmphasized', +} as const satisfies Record; + +const itemColors = { + activeIndicatorColor: 'secondaryContainer', + activeIconColor: 'onSecondaryContainer', + activeLabelTextColor: 'onSecondaryContainer', + inactiveIconColor: 'onSurfaceVariant', + inactiveLabelTextColor: 'onSurfaceVariant', + inactiveFocusIconColor: 'onSurface', + inactiveFocusLabelTextColor: 'onSurface', + focusIndicatorColor: 'secondary', +} as const satisfies Record; + +const { thickness, innerOffset } = tokens.md.sys.state.focusIndicator; + +const itemFocusIndicator = { + thickness, + inset: -innerOffset, +} as const; + +export const DrawerItemTokens = { + ...itemSizes, + ...itemShape, + ...itemTypescale, + ...itemColors, + focusIndicator: itemFocusIndicator, + stateOpacity, +}; + +const sectionSizes = { + headlineHeight: 56, + headlinePadding: 28, + bottomSpacing: 4, + dividerSpacing: 4, +} as const; + +const sectionTypescale = { + headlineText: 'titleSmall', +} as const satisfies Record; + +const sectionColors = { + headlineColor: 'onSurfaceVariant', +} as const satisfies Record; + +export const DrawerSectionTokens = { + ...sectionSizes, + ...sectionTypescale, + ...sectionColors, +}; + +const collapsedItemSizes = { + containerWidth: 80, + iconSize: 24, + activeIndicatorWidth: 56, + activeIndicatorHeight: 32, + noLabelActiveIndicatorHeight: 56, + labelSpacing: 4, + labelPadding: 12, + containerSpacing: 12, +} as const; + +const collapsedItemShape = { + activeIndicatorShape: 'full', +} as const satisfies Record; + +const collapsedItemMotion = { + activeIndicatorDuration: 'short3', + activeIndicatorEasing: 'standard', +} as const satisfies { + activeIndicatorDuration: keyof MotionDuration; + activeIndicatorEasing: keyof MotionEasing; +}; + +const collapsedItemTypescale = { + labelText: 'labelMedium', +} as const satisfies Record; + +const collapsedItemColors = { + activeIndicatorColor: 'secondaryContainer', + activeIconColor: 'onSecondaryContainer', + activeLabelTextColor: 'onSurface', + inactiveIconColor: 'onSurfaceVariant', + inactiveLabelTextColor: 'onSurfaceVariant', +} as const satisfies Record; + +export const DrawerCollapsedItemTokens = { + ...collapsedItemSizes, + ...collapsedItemShape, + ...collapsedItemMotion, + ...collapsedItemTypescale, + ...collapsedItemColors, + stateOpacity, +}; diff --git a/src/components/__tests__/Drawer/DrawerCollapsedItem.test.tsx b/src/components/__tests__/Drawer/DrawerCollapsedItem.test.tsx index 3df20be862..4b50ae7141 100644 --- a/src/components/__tests__/Drawer/DrawerCollapsedItem.test.tsx +++ b/src/components/__tests__/Drawer/DrawerCollapsedItem.test.tsx @@ -1,8 +1,11 @@ import { describe, expect, it } from '@jest/globals'; import { render, screen } from '../../../test-utils'; +import { tokens } from '../../../theme/tokens'; import DrawerCollapsedItem from '../../Drawer/DrawerCollapsedItem'; +const stateOpacity = tokens.md.sys.state.opacity; + describe('DrawerCollapsedItem', () => { it('should have regular outline if label is specified', async () => { await render( @@ -65,4 +68,22 @@ describe('DrawerCollapsedItem', () => { .props.source ).toBe('star'); }); + + it('should render at full opacity when enabled', async () => { + await render(); + + expect(screen.getByTestId('drawer-collapsed-item')).toHaveStyle({ + opacity: stateOpacity.enabled, + }); + }); + + it('should dim the destination when disabled', async () => { + await render( + + ); + + expect(screen.getByTestId('drawer-collapsed-item')).toHaveStyle({ + opacity: stateOpacity.disabled, + }); + }); }); diff --git a/src/components/__tests__/Drawer/DrawerSection.test.tsx b/src/components/__tests__/Drawer/DrawerSection.test.tsx index 9b19f0ef25..5c4ae43319 100644 --- a/src/components/__tests__/Drawer/DrawerSection.test.tsx +++ b/src/components/__tests__/Drawer/DrawerSection.test.tsx @@ -2,7 +2,8 @@ import { View } from 'react-native'; import { describe, expect, it } from '@jest/globals'; -import { render } from '../../../test-utils'; +import { defaultThemes } from '../../../core/theming'; +import { render, screen } from '../../../test-utils'; import DrawerSection from '../../Drawer/DrawerSection'; describe('DrawerSection', () => { @@ -17,4 +18,52 @@ describe('DrawerSection', () => { expect(tree).toMatchSnapshot(); }); + + it('separates sections with a themed divider', async () => { + await render( + + + + ); + + expect(screen.getByTestId('drawer-section-divider')).toHaveStyle({ + backgroundColor: defaultThemes.light.colors.outlineVariant, + }); + }); + + it('follows a custom theme for the divider colour', async () => { + const theme = { + colors: { outlineVariant: 'rgb(1, 2, 3)' }, + }; + + await render( + + + + ); + + expect(screen.getByTestId('drawer-section-divider')).toHaveStyle({ + backgroundColor: 'rgb(1, 2, 3)', + }); + }); + + it('omits the divider when showDivider is false', async () => { + await render( + + + + ); + + expect(screen.queryByTestId('drawer-section-divider')).toBeNull(); + }); + + it('aligns the section title with the destination icons', async () => { + await render( + + + + ); + + expect(screen.getByText('Mailboxes')).toHaveStyle({ marginStart: 28 }); + }); }); diff --git a/src/components/__tests__/Drawer/__snapshots__/DrawerSection.test.tsx.snap b/src/components/__tests__/Drawer/__snapshots__/DrawerSection.test.tsx.snap index bcd35191f0..16c47a4192 100644 --- a/src/components/__tests__/Drawer/__snapshots__/DrawerSection.test.tsx.snap +++ b/src/components/__tests__/Drawer/__snapshots__/DrawerSection.test.tsx.snap @@ -27,16 +27,12 @@ exports[`DrawerSection renders properly 1`] = ` { "height": 1, }, - [ - { - "marginTop": 4, - }, - { - "backgroundColor": "rgba(121, 116, 126, 1)", - }, - ], + { + "marginTop": 4, + }, ] } + testID="drawer-section-divider" /> `; diff --git a/src/components/__tests__/DrawerItem.test.tsx b/src/components/__tests__/DrawerItem.test.tsx index a47bbc043b..c337c6e68f 100644 --- a/src/components/__tests__/DrawerItem.test.tsx +++ b/src/components/__tests__/DrawerItem.test.tsx @@ -1,8 +1,13 @@ import { expect, it } from '@jest/globals'; -import { render } from '../../test-utils'; +import { defaultThemes } from '../../core/theming'; +import { fireEvent, render, screen } from '../../test-utils'; +import { tokens } from '../../theme/tokens'; import DrawerItem from '../Drawer/DrawerItem'; +const { colors } = defaultThemes.light; +const stateOpacity = tokens.md.sys.state.opacity; + it('renders basic DrawerItem', async () => { const tree = ( await render( {}} label="Example item" />) @@ -26,3 +31,110 @@ it('renders active DrawerItem', async () => { expect(tree).toMatchSnapshot(); }); + +it('emphasizes the label of the active destination', async () => { + await render(); + + expect(screen.getByText('Active item')).toHaveStyle({ fontWeight: '700' }); +}); + +it('does not emphasize the label of an inactive destination', async () => { + await render(); + + expect(screen.getByText('Inactive item')).toHaveStyle({ fontWeight: '500' }); +}); + +it('marks the active destination with a filled indicator', async () => { + await render(); + + expect(screen.getByRole('button')).toHaveStyle({ + backgroundColor: colors.secondaryContainer, + }); +}); + +it('leaves an inactive destination without an indicator', async () => { + await render(); + + expect(screen.getByRole('button')).toHaveStyle({ + backgroundColor: undefined, + }); +}); + +it('insets destination content 28dp from both drawer edges', async () => { + await render(); + + expect(screen.getByRole('button')).toHaveStyle({ marginHorizontal: 12 }); + expect(screen.getByTestId('drawer-item-content')).toHaveStyle({ + marginHorizontal: 16, + }); +}); + +it('shows a focus indicator while focused', async () => { + await render( {}} />); + + expect(screen.queryByTestId('drawer-item-focus-ring')).toBeNull(); + + await fireEvent(screen.getByRole('button'), 'focus'); + expect(screen.getByTestId('drawer-item-focus-ring')).toHaveStyle({ + borderColor: colors.secondary, + }); + + await fireEvent(screen.getByRole('button'), 'blur'); + expect(screen.queryByTestId('drawer-item-focus-ring')).toBeNull(); +}); + +it('strengthens the label of an inactive destination while focused', async () => { + await render( {}} />); + + expect(screen.getByText('Example item')).toHaveStyle({ + color: colors.onSurfaceVariant, + }); + + await fireEvent(screen.getByRole('button'), 'focus'); + expect(screen.getByText('Example item')).toHaveStyle({ + color: colors.onSurface, + }); +}); + +it('keeps the label of an active destination while focused', async () => { + await render( {}} />); + + await fireEvent(screen.getByRole('button'), 'focus'); + expect(screen.getByText('Example item')).toHaveStyle({ + color: colors.onSecondaryContainer, + }); +}); + +it('renders an enabled destination at full opacity', async () => { + await render( {}} />); + + expect(screen.getByRole('button')).toHaveStyle({ + opacity: stateOpacity.enabled, + }); +}); + +it('dims a disabled destination', async () => { + await render( {}} disabled />); + + expect(screen.getByRole('button')).toHaveStyle({ + opacity: stateOpacity.disabled, + }); +}); + +it('does not show a focus indicator on a disabled destination', async () => { + await render( {}} disabled />); + + await fireEvent(screen.getByRole('button'), 'focus'); + expect(screen.queryByTestId('drawer-item-focus-ring')).toBeNull(); +}); + +it('dims the active indicator of a disabled destination', async () => { + await render( + {}} active disabled /> + ); + + expect(screen.getByRole('button')).toHaveStyle({ + backgroundColor: colors.secondaryContainer, + opacity: stateOpacity.disabled, + }); +}); diff --git a/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap b/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap index 4823f954c1..6adbb0d25e 100644 --- a/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DrawerItem.test.tsx.snap @@ -39,21 +39,17 @@ exports[`renders DrawerItem with icon 1`] = ` "overflow": "hidden", }, [ - { - "marginHorizontal": 10, - "marginVertical": 4, - }, { "height": 56, "justifyContent": "center", - "marginLeft": 12, - "marginRight": 12, - "marginVertical": 0, + "marginHorizontal": 12, }, { "backgroundColor": undefined, - "borderRadius": 28, + "borderRadius": 9999, + "opacity": 1, }, + null, undefined, ], ] @@ -61,98 +57,98 @@ exports[`renders DrawerItem with icon 1`] = ` > - - information - - + information + + - Example item - + ] + } + > + Example item +
+ @@ -198,21 +194,17 @@ exports[`renders active DrawerItem 1`] = ` "overflow": "hidden", }, [ - { - "marginHorizontal": 10, - "marginVertical": 4, - }, { "height": 56, "justifyContent": "center", - "marginLeft": 12, - "marginRight": 12, - "marginVertical": 0, + "marginHorizontal": 12, }, { "backgroundColor": "rgba(232, 222, 248, 1)", - "borderRadius": 28, + "borderRadius": 9999, + "opacity": 1, }, + null, undefined, ], ] @@ -220,98 +212,98 @@ exports[`renders active DrawerItem 1`] = ` > - - information - - + information + + - Example item - + ] + } + > + Example item + + @@ -357,21 +349,17 @@ exports[`renders basic DrawerItem 1`] = ` "overflow": "hidden", }, [ - { - "marginHorizontal": 10, - "marginVertical": 4, - }, { "height": 56, "justifyContent": "center", - "marginLeft": 12, - "marginRight": 12, - "marginVertical": 0, + "marginHorizontal": 12, }, { "backgroundColor": undefined, - "borderRadius": 28, + "borderRadius": 9999, + "opacity": 1, }, + null, undefined, ], ] @@ -379,69 +367,67 @@ exports[`renders basic DrawerItem 1`] = ` > - + - Example item - + ] + } + > + Example item + + diff --git a/src/theme/tokens/sys/state.ts b/src/theme/tokens/sys/state.ts index d0742351bf..c3a9d8ac78 100644 --- a/src/theme/tokens/sys/state.ts +++ b/src/theme/tokens/sys/state.ts @@ -14,5 +14,6 @@ export const state = { focusIndicator: { thickness: 3, outerOffset: 2, + innerOffset: -3, }, } as const; diff --git a/src/theme/types/typography.ts b/src/theme/types/typography.ts index 57fcf3f67e..f7ad73b80f 100644 --- a/src/theme/types/typography.ts +++ b/src/theme/types/typography.ts @@ -37,7 +37,22 @@ export type TypescaleKey = | 'labelSmall' | 'bodyLarge' | 'bodyMedium' - | 'bodySmall'; + | 'bodySmall' + | 'displayLargeEmphasized' + | 'displayMediumEmphasized' + | 'displaySmallEmphasized' + | 'headlineLargeEmphasized' + | 'headlineMediumEmphasized' + | 'headlineSmallEmphasized' + | 'titleLargeEmphasized' + | 'titleMediumEmphasized' + | 'titleSmallEmphasized' + | 'labelLargeEmphasized' + | 'labelMediumEmphasized' + | 'labelSmallEmphasized' + | 'bodyLargeEmphasized' + | 'bodyMediumEmphasized' + | 'bodySmallEmphasized'; export type TypescaleStyle = { fontFamily: string;