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 (
+
+
+
+
+
+
+
+
+
+ }
+ />
+ (
+
+ )}
+ />
+
+
+
+
+
+
+
+ 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;