Introduce NUX element
Summary:
allow-large-files
This diff introces the `NUX` element that can be wrapped around any other element to give a first-time usage hint.
Hint dismissal is stored by taking a hash of the hint contents, and scoped per plugin.
Users can reset the 'read' status in the settings page
Example usage:
```
<NUX
title="Use bookmarks to directly navigate to a location in the app."
placement="right">
<Input addonAfter={<SettingOutlined />} defaultValue="mysite" />
</NUX>
```
Reviewed By: nikoant
Differential Revision: D24622276
fbshipit-source-id: 0265634f9ab50c32214b74f033f59482cd986f23
This commit is contained in:
committed by
Facebook GitHub Bot
parent
b8b9c4296a
commit
2b0e93a063
@@ -9,14 +9,20 @@
|
||||
"license": "MIT",
|
||||
"bugs": "https://github.com/facebook/flipper/issues",
|
||||
"dependencies": {
|
||||
"@testing-library/dom": "^7.26.3",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"immer": "^7.0.5"
|
||||
"react-element-to-jsx-string": "^14.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.3",
|
||||
"typescript": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ant-design/icons": "^4.2.2",
|
||||
"@testing-library/dom": "^7.26.3",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"antd": "^4.8.0",
|
||||
"emotion": "^10.0.27",
|
||||
"immer": "^7.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"reset": "rimraf lib *.tsbuildinfo",
|
||||
"build": "tsc -b",
|
||||
|
||||
@@ -34,6 +34,7 @@ export {
|
||||
|
||||
export {theme} from './ui/theme';
|
||||
export {Layout} from './ui/Layout';
|
||||
export {NUX, NuxManagerContext, createNuxManager} from './ui/NUX';
|
||||
|
||||
// It's not ideal that this exists in flipper-plugin sources directly,
|
||||
// but is the least pain for plugin authors.
|
||||
|
||||
@@ -198,7 +198,7 @@ const SandySplitContainer = styled.div<{
|
||||
flex: 1,
|
||||
flexDirection: props.flexDirection,
|
||||
alignItems: props.center ? 'center' : 'stretch',
|
||||
overflow: 'hidden',
|
||||
overflow: props.center ? undefined : 'hidden', // only use overflow hidden in container mode, to avoid weird resizing issues
|
||||
'> :nth-child(1)': {
|
||||
flex: props.grow === 1 ? splitGrowStyle : splitFixedStyle,
|
||||
minWidth: props.grow === 1 ? 0 : undefined,
|
||||
|
||||
158
desktop/flipper-plugin/src/ui/NUX.tsx
Normal file
158
desktop/flipper-plugin/src/ui/NUX.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import React, {createContext, useCallback, useContext} from 'react';
|
||||
import {Badge, Tooltip, Typography, Button} from 'antd';
|
||||
import styled from '@emotion/styled';
|
||||
import {SandyPluginInstance, theme} from 'flipper-plugin';
|
||||
import {keyframes} from 'emotion';
|
||||
import reactElementToJSXString from 'react-element-to-jsx-string';
|
||||
import {SandyPluginContext} from '../plugin/PluginContext';
|
||||
import {createState, useValue} from '../state/atom';
|
||||
import {SandyDevicePluginInstance} from '../plugin/DevicePlugin';
|
||||
import {Layout} from './Layout';
|
||||
import {BulbTwoTone} from '@ant-design/icons';
|
||||
import {createHash} from 'crypto';
|
||||
import type {TooltipPlacement} from 'antd/lib/tooltip';
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
type NuxManager = ReturnType<typeof createNuxManager>;
|
||||
|
||||
const storageKey = `FLIPPER_NUX_STATE`;
|
||||
|
||||
export function getNuxKey(
|
||||
elem: React.ReactNode,
|
||||
currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance,
|
||||
) {
|
||||
return `${currentPlugin?.definition.id ?? 'flipper'}:${createHash('sha256')
|
||||
.update(reactElementToJSXString(elem))
|
||||
.digest('base64')}`;
|
||||
}
|
||||
|
||||
export function createNuxManager() {
|
||||
const ticker = createState(0);
|
||||
|
||||
let readMap: Record<string, boolean> = JSON.parse(
|
||||
window.localStorage.getItem(storageKey) || '{}',
|
||||
);
|
||||
|
||||
function save() {
|
||||
// trigger all Nux Elements to re-compute state
|
||||
ticker.set(ticker.get() + 1);
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(readMap, null, 2));
|
||||
}
|
||||
|
||||
return {
|
||||
markRead(
|
||||
elem: React.ReactNode,
|
||||
currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance,
|
||||
): void {
|
||||
readMap[getNuxKey(elem, currentPlugin)] = true;
|
||||
save();
|
||||
},
|
||||
isRead(
|
||||
elem: React.ReactNode,
|
||||
currentPlugin?: SandyPluginInstance | SandyDevicePluginInstance,
|
||||
): boolean {
|
||||
return !!readMap[getNuxKey(elem, currentPlugin)];
|
||||
},
|
||||
resetHints(): void {
|
||||
readMap = {};
|
||||
save();
|
||||
},
|
||||
ticker,
|
||||
};
|
||||
}
|
||||
|
||||
const stubManager: NuxManager = {
|
||||
markRead() {},
|
||||
isRead() {
|
||||
return true;
|
||||
},
|
||||
resetHints() {},
|
||||
ticker: createState(0),
|
||||
};
|
||||
|
||||
export const NuxManagerContext = createContext<NuxManager>(stubManager);
|
||||
|
||||
/**
|
||||
* Creates a New-User-eXperience element; a lightbulb that will show the user new features
|
||||
*/
|
||||
export function NUX({
|
||||
children,
|
||||
title,
|
||||
placement,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
placement?: TooltipPlacement;
|
||||
}) {
|
||||
const manager = useContext(NuxManagerContext);
|
||||
const pluginInstance = useContext(SandyPluginContext);
|
||||
// changing the ticker will force `isRead` to be recomputed
|
||||
const _tick = useValue(manager.ticker);
|
||||
const isRead = manager.isRead(title, pluginInstance);
|
||||
const dismiss = useCallback(() => {
|
||||
manager.markRead(title, pluginInstance);
|
||||
}, [title, manager, pluginInstance]);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
count={
|
||||
isRead ? (
|
||||
0
|
||||
) : (
|
||||
<Tooltip
|
||||
placement={placement}
|
||||
color={theme.backgroundWash}
|
||||
title={
|
||||
<Layout.Container
|
||||
center
|
||||
gap
|
||||
pad
|
||||
style={{color: theme.textColorPrimary}}>
|
||||
<BulbTwoTone style={{fontSize: 24}} />
|
||||
<Text>{title}</Text>
|
||||
<Button size="small" type="default" onClick={dismiss}>
|
||||
Dismiss
|
||||
</Button>
|
||||
</Layout.Container>
|
||||
}>
|
||||
<Pulse />
|
||||
</Tooltip>
|
||||
)
|
||||
}>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
const pulse = keyframes({
|
||||
'0%': {
|
||||
opacity: 0.2,
|
||||
},
|
||||
'100%': {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const Pulse = styled.div({
|
||||
cursor: 'pointer',
|
||||
background: theme.warningColor,
|
||||
animation: `${pulse} 2s infinite alternate`,
|
||||
borderRadius: 20,
|
||||
height: 12,
|
||||
width: 12,
|
||||
':hover': {
|
||||
opacity: `1 !important`,
|
||||
background: theme.errorColor,
|
||||
animationPlayState: 'paused',
|
||||
},
|
||||
});
|
||||
47
desktop/flipper-plugin/src/ui/__tests__/NUX.node.tsx
Normal file
47
desktop/flipper-plugin/src/ui/__tests__/NUX.node.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {TestUtils} from 'flipper-plugin';
|
||||
import React from 'react';
|
||||
import {getNuxKey} from '../NUX';
|
||||
|
||||
test('nuxkey computation', () => {
|
||||
expect(getNuxKey('test')).toMatchInlineSnapshot(
|
||||
`"flipper:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="`,
|
||||
);
|
||||
expect(getNuxKey('test')).toMatchInlineSnapshot(
|
||||
`"flipper:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="`,
|
||||
);
|
||||
expect(getNuxKey('test2')).toMatchInlineSnapshot(
|
||||
`"flipper:YDA64iuZiGG847KPM+7BvnWKITyGyTwHbb6fVYwRx1I="`,
|
||||
);
|
||||
expect(getNuxKey(<div>bla</div>)).toMatchInlineSnapshot(
|
||||
`"flipper:myN0Mqqzs3fPwYDKGEQVG9XD9togJNWYJiy1VNQOf18="`,
|
||||
);
|
||||
expect(getNuxKey(<div>bla2</div>)).toMatchInlineSnapshot(
|
||||
`"flipper:B6kICeYCJMWeUThs5TWCLuiwCqzr5cWn67xXA4ET0bU="`,
|
||||
);
|
||||
});
|
||||
|
||||
test('nuxkey computation with plugin', () => {
|
||||
const res = TestUtils.startPlugin({
|
||||
Component() {
|
||||
return null;
|
||||
},
|
||||
plugin() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
getNuxKey('test', (res as any)._backingInstance),
|
||||
).toMatchInlineSnapshot(
|
||||
`"TestPlugin:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="`,
|
||||
);
|
||||
});
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
export const theme = {
|
||||
white: 'white', // use as counter color for primary
|
||||
black: 'black',
|
||||
primaryColor: 'var(--flipper-primary-color)',
|
||||
successColor: 'var(--flipper-success-color)',
|
||||
errorColor: 'var(--flipper-error-color)',
|
||||
|
||||
Reference in New Issue
Block a user