Open lukewalczak opened 12 months ago
@lukewalczak What the current state of this feature, is it developed already?
@lukewalczak What the current state of this feature, is it developed already?
It's ready to be picked up 🙂
@lukewalczak can you please provide me any tentative timelines for the release of this component?
I apologize for any confusion. What I intended to convey is that anyone willing to take on the task can handle the feature and develop a new Tooltip
type.
@lukewalczak it's okay, I thought your team is going to pick this item.
@lukewalczak Can you please guide me how to start implementing the tooltip component from scratch, what other component, logic I can reuse to build it.
From the internal component as a base, you should use Surface
component, for the title and supporting texts the Text
component with the appropriate variant
indicated in the specs. For the actions just use Button
with a default mode="text"
.
Hi @lukewalczak do you have this in the works? or can I pick this up?
I took a stab at this and it's difficult for two reasons:
Surface
is hard to use as it doesn't act like a View
. In particular it requires a fixed width
while you ideally want to set a maxWidth
instead (320 seems to be the max width of a rich tooltip) so the tool tip size scales with the content.FAB
doesn't work as the FAB
doesn't, unlike an absolutely positioned View
, report correct pageX
and pageY
when calling measure()
on it to decide where to place the tooltip. This again seems to be related to the (internal) use of Surface
in FAB
.Edit: a quick test shows that Tooltip
doesn't work with FAB
either (it's position on the screen is wrong and far away from the FAB, probably due to incorrectly measuring the FAB as being as wide as the screen).
Here's an initial working prototype.
It uses a mix of the approaches used in Tooltip
and Menu
to work correctly with absolutely positioned children (which Tooltip
doesn't seem to work correctly with from my testing).
Possible improvements:
actions
prop much attention (the MD3 spec doesn't look like it uses normal text buttons but doesn't spec the buttons further).onLayout
on the tooltip itself to compute the position of the children
(same as Tooltip
does) doesn't work if the position of the children
change subsequently.children
needs to accept a ref
of type View
. I tried to use the same wrapper approach as in Tooltip
but that didn't work for me (and doesn't seem to work correctly in Tooltip
either).import React from 'react';
import {
Dimensions,
LayoutChangeEvent,
LayoutRectangle,
StyleSheet,
View,
} from 'react-native';
import {Portal, Surface, Text, useTheme} from 'react-native-paper';
// react-native paper currently lacks a rich tooltip component:
// https://github.com/callstack/react-native-paper/issues/4074
type ChildrenMeasurement = {
width: number;
height: number;
pageX: number;
pageY: number;
};
type TooltipLayout = LayoutRectangle;
export type Measurement = {
children: ChildrenMeasurement;
tooltip: TooltipLayout;
measured: boolean;
};
/**
* Return true when the tooltip center x-coordinate relative to the wrapped element is negative.
* The tooltip will be placed at the starting x-coordinate from the wrapped element.
*/
const overflowLeft = (childrenX: number, tooltipWidth: number): boolean => {
return childrenX - tooltipWidth < 0;
};
/**
* Return true when the tooltip center x-coordinate + tooltip width is greater than the layout width
* The tooltip width will grow from right to left relative to the wrapped element.
*/
const overflowRight = (
childrenX: number,
childrenWidth: number,
tooltipWidth: number,
): boolean => {
const {width: layoutWidth} = Dimensions.get('window');
return childrenX + childrenWidth + tooltipWidth > layoutWidth;
};
/**
* Return true when the children y-coordinate + its height + tooltip height is greater than the layout height.
* The tooltip will be placed at the top of the wrapped element.
*/
const overflowBottom = (
childrenY: number,
childrenHeight: number,
tooltipHeight: number,
): boolean => {
const {height: layoutHeight} = Dimensions.get('window');
return childrenY + childrenHeight + tooltipHeight > layoutHeight;
};
const getTooltipXPosition = (
{pageX: childrenX, width: childrenWidth}: ChildrenMeasurement,
{width: tooltipWidth}: TooltipLayout,
): number => {
if (overflowRight(childrenX, childrenWidth, tooltipWidth)) {
if (overflowLeft(childrenX, tooltipWidth)) {
const {width: layoutWidth} = Dimensions.get('window');
return layoutWidth - tooltipWidth;
}
return childrenX - tooltipWidth;
}
return childrenX + childrenWidth;
};
const getTooltipYPosition = (
{pageY: childrenY, height: childrenHeight}: ChildrenMeasurement,
{height: tooltipHeight}: TooltipLayout,
): number => {
if (overflowBottom(childrenY, childrenHeight, tooltipHeight)) {
return childrenY - tooltipHeight;
}
// We assume that we can't both overflow bottom and top.
return childrenY + childrenHeight;
};
export const getTooltipPosition = ({
children,
tooltip,
measured,
}: Measurement): Record<string, never> | {left: number; top: number} => {
if (!measured) {
return {};
}
return {
left: getTooltipXPosition(children, tooltip),
top: getTooltipYPosition(children, tooltip),
};
};
type Props = {
actions?: React.ReactElement[];
children: React.ReactElement;
subhead?: string;
supportingText: string;
visible?: boolean;
};
/**
* Material Design 3 rich tooltip.
*
* Note that `children` must be a React element with a ref of type `View`.
*
* See https://m3.material.io/components/tooltips/overview.
*/
const RichTooltip = ({
actions = [],
children,
subhead,
supportingText,
visible = false,
}: Props): JSX.Element => {
const theme = useTheme();
const [measurement, setMeasurement] = React.useState({
children: {},
tooltip: {},
measured: false,
});
const childrenRef = React.useRef() as React.MutableRefObject<View>;
const handleOnLayout = ({nativeEvent: {layout}}: LayoutChangeEvent) => {
childrenRef.current.measureInWindow((pageX, pageY, width, height) => {
setMeasurement({
children: {pageX, pageY, height, width},
tooltip: {...layout},
measured: true,
});
});
};
return (
<>
{visible && (
<Portal>
<Surface
elevation={2}
onLayout={handleOnLayout}
style={[
styles.surface,
{
...getTooltipPosition(measurement as Measurement),
...(measurement.measured ? styles.visible : styles.hidden),
},
]}
testID="tooltip-container">
{subhead ? (
<Text
style={[styles.subhead, {color: theme.colors.onSurfaceVariant}]}
variant="titleSmall">
{subhead}
</Text>
) : null}
<Text
style={{color: theme.colors.onSurfaceVariant}}
variant="bodyMedium">
{supportingText}
</Text>
{actions.length > 0 ? (
<View style={styles.actions}>
{actions.map((action, index) => (
<View key={index}>{action}</View>
))}
</View>
) : null}
</Surface>
</Portal>
)}
{React.cloneElement(children, {ref: childrenRef})}
</>
);
};
export default RichTooltip;
const styles = StyleSheet.create({
actions: {
flexDirection: 'row',
marginTop: 12,
},
surface: {
alignSelf: 'flex-start',
borderRadius: 12,
paddingTop: 12,
paddingBottom: 8,
paddingHorizontal: 16,
// The MD3 spec doesn't specify the width but this is the max width of the
// tooltip used in the Jetpack implementation. This includes the padding.
maxWidth: 320 - 2 * 16,
},
subhead: {
marginBottom: 4,
},
visible: {
opacity: 1,
},
hidden: {
opacity: 0,
},
});
Is your feature request related to a problem? Please describe.
According to the Material Design documentation there is new type for
Tooltip
component, calledrich
.https://m3.material.io/components/tooltips/overview
Describe the solution you'd like
Follow component anatomy from: https://m3.material.io/components/tooltips/guidelines
Create all possible variants:
Additional context
Should be available in both MD generations.