Skip to content

Commit

Permalink
fixup! chore(react-keytips): draft implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
mainframev committed Aug 28, 2024
1 parent cf0ef38 commit 9e76165
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 70 deletions.
4 changes: 2 additions & 2 deletions packages/react-keytips/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"@swc/helpers": "~0.5.2",
"@fluentui/react-jsx-runtime": ">=9.0.29 < 10.0.0",
"@fluentui/react-utilities": ">=9.16.0 < 10.0.0",
"@fluentui/keyboard-keys": "~9.0.6",
"@fluentui/react-tooltip": "^9.4.33"
"@fluentui/react-positioning": ">=9.16.0 < 10.0.0",
"@fluentui/keyboard-keys": "~9.0.6"
},
"main": "./src/index.js",
"typings": "./src/index.d.ts",
Expand Down
3 changes: 3 additions & 0 deletions packages/react-keytips/src/components/Keytip/Keytip.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useKeytip_unstable } from './useKeytip';
import { renderKeytip_unstable } from './renderKeytip';
import type { KeytipProps } from './Keytip.types';
import { useKeytipStyles_unstable } from './useKeytipStyles.styles';

/**
* Keytip component. Responsible for rendering an individual keytip,
Expand All @@ -9,6 +10,8 @@ import type { KeytipProps } from './Keytip.types';
*/
export const Keytip = (props: KeytipProps) => {
const state = useKeytip_unstable(props);
useKeytipStyles_unstable(state);

return renderKeytip_unstable(state);
};

Expand Down
61 changes: 50 additions & 11 deletions packages/react-keytips/src/components/Keytip/Keytip.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import type { EventData, EventHandler } from '@fluentui/react-utilities';
import type { PositioningProps } from '@fluentui/react-components';
import type { TooltipProps } from '@fluentui/react-tooltip';
import type { PositioningShorthand } from '@fluentui/react-components';
import type {
ComponentProps,
ComponentState,
Slot,
} from '@fluentui/react-utilities';

/**
* Slot properties for Keytip
*/
export type KeytipSlots = {
/**
* The text or JSX content of the Keytip.
*/
content: NonNullable<Slot<'div'>>;
};

export type ExecuteKeytipEventHandler<E = HTMLElement> = EventHandler<
EventData<'keydown', KeyboardEvent> & {
Expand All @@ -14,15 +28,34 @@ export type ReturnKeytipEventHandler<E = HTMLElement> = EventHandler<
}
>;

export type KeytipProps = Pick<
TooltipProps,
'appearance' | 'visible' | 'content'
> & {
export type OnVisibleChangeData = {
visible: boolean;

/**
* The event object, if this visibility change was triggered by a keyboard event on the document element
* (such as Escape to hide the visible Keytip). Otherwise undefined.
*/
documentKeyboardEvent?: KeyboardEvent;
};

export type KeytipProps = ComponentProps<KeytipSlots> & {
/**
* Positioning props to be passed to Keytip Tooltip.
* Positioning props to be passed to Keytip.
* @default { align: 'center', position: 'below' }
*/
positioning?: PositioningProps;
positioning?: PositioningShorthand;
/**
* The keytip's visual appearance.
* * `normal` - Uses the theme's background and text colors.
* * `inverted` - Higher contrast variant that uses the theme's inverted colors.
*
* @default inverted
*/
appearance?: 'inverted' | 'normal';
/**
* Whether the keytip is visible.
*/
visible?: boolean;
/**
* Function to call when this keytip is activated.
*/
Expand All @@ -45,6 +78,12 @@ export type KeytipProps = Pick<

export type KeytipWithId = KeytipProps & { uniqueId: string };

export type KeytipState = Required<
Pick<KeytipProps, 'visible' | 'positioning' | 'appearance' | 'content'>
>;
export type KeytipState = ComponentState<KeytipSlots> &
Required<
Pick<KeytipProps, 'visible' | 'positioning' | 'appearance' | 'content'>
> & {
/**
* Whether the Keytip should be rendered to the DOM.
*/
shouldRenderTooltip?: boolean;
};
19 changes: 6 additions & 13 deletions packages/react-keytips/src/components/Keytip/renderKeytip.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
/** @jsxRuntime classic */
/** @jsx createElement */

import type { KeytipState } from './Keytip.types';
import { Tooltip } from '@fluentui/react-tooltip';
import type { KeytipSlots, KeytipState } from './Keytip.types';
import { assertSlots } from '@fluentui/react-utilities';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { createElement } from '@fluentui/react-jsx-runtime';

/**
* Render the final JSX of Keytip
*/
export const renderKeytip_unstable = (state: KeytipState) => {
const { visible, appearance, positioning, content } = state;

return (
<Tooltip
visible={visible}
appearance={appearance}
positioning={positioning}
content={content}
relationship="description"
/>
);
assertSlots<KeytipSlots>(state);
return state.shouldRenderTooltip ? (
<state.content>{state.content.children}</state.content>
) : null;
};
77 changes: 63 additions & 14 deletions packages/react-keytips/src/components/Keytip/useKeytip.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import * as React from 'react';
import type { KeytipProps, KeytipState } from './Keytip.types';
import type { TooltipProps } from '@fluentui/react-tooltip';
import { sequencesToID } from '../../utilities';
import { useFluent } from '@fluentui/react-components';
import { DATAKTP_TARGET } from '../../constants';
import {
resolvePositioningShorthand,
usePositioning,
} from '@fluentui/react-positioning';
import {
slot,
useId,
useIsSSR,
useMergedRefs,
} from '@fluentui/react-utilities';

const TOOLTIP_BORDER_RADIUS = 4;

/**
* Create the state required to render Keytip.
*
Expand All @@ -11,33 +24,69 @@ import { DATAKTP_TARGET } from '../../constants';
*
* @param props - props from this instance of Keytip
*/

export const useKeytip_unstable = (props: KeytipProps): KeytipState => {
'use no memo';

const {
positioning,
visible = false,
positioning = { align: 'center', position: 'below' },
keySequences,
content,
visible = false,
appearance = 'inverted',
} = props;
const defaultPositioning: TooltipProps['positioning'] = {
align: 'center',
position: 'below',
...positioning,
};

const isServerSideRender = useIsSSR();
const { targetDocument } = useFluent();
const id = sequencesToID(keySequences);
const target = targetDocument?.querySelector(`[${DATAKTP_TARGET}="${id}"]`);

const state: KeytipState = {
components: {
content: 'div',
},
positioning,
shouldRenderTooltip: visible,
visible,
content,
appearance,
positioning: {
target,
...defaultPositioning,
},
content: slot.always(content, {
defaultProps: {
role: 'tooltip',
},
elementType: 'div',
}),
};

state.content.id = useId('keytip-', state.content.id);

const positioningOptions = {
enabled: state.visible,
arrowPadding: 2 * TOOLTIP_BORDER_RADIUS,
position: 'below' as const,
align: 'center' as const,
target,
offset: 4,
...resolvePositioningShorthand(state.positioning),
};

const {
containerRef,
}: {
targetRef: React.MutableRefObject<HTMLElement>;
containerRef: React.MutableRefObject<HTMLDivElement>;
} = usePositioning(positioningOptions);

state.content.ref = useMergedRefs(state.content.ref, containerRef);

if (typeof state.content.children === 'string') {
target?.setAttribute('aria-label', state.content.children);
} else {
target?.setAttribute('aria-labelledby', state.content.id);
state.shouldRenderTooltip = true;
}

if (isServerSideRender) {
state.shouldRenderTooltip = false;
}

return state;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { tokens, makeStyles, mergeClasses } from '@fluentui/react-components';
import type { SlotClassNames } from '@fluentui/react-utilities';
import { KeytipSlots, KeytipState } from './Keytip.types';

export const keytipClassNames: SlotClassNames<KeytipSlots> = {
content: 'fui-Keytip__content',
};

/**
* Styles for the tooltip
*/
const useStyles = makeStyles({
root: {
display: 'none',
boxSizing: 'border-box',
maxWidth: '240px',
cursor: 'default',
fontFamily: tokens.fontFamilyBase,
fontSize: tokens.fontSizeBase200,
lineHeight: tokens.lineHeightBase200,
overflowWrap: 'break-word',
borderRadius: tokens.borderRadiusMedium,
border: `1px solid ${tokens.colorTransparentStroke}`,
padding: '4px 11px 6px 11px', // '5px 12px 7px 12px' minus the border width '1px'
backgroundColor: tokens.colorNeutralBackground1,
color: tokens.colorNeutralForeground1,

// TODO need to add versions of tokens.alias.shadow.shadow8, etc. that work with filter
filter:
`drop-shadow(0 0 2px ${tokens.colorNeutralShadowAmbient}) ` +
`drop-shadow(0 4px 8px ${tokens.colorNeutralShadowKey})`,
},

visible: {
display: 'block',
},

inverted: {
backgroundColor: tokens.colorNeutralBackgroundStatic,
color: tokens.colorNeutralForegroundStaticInverted,
},
});

/**
* Apply styling to the Tooltip slots based on the state
*/
export const useKeytipStyles_unstable = (state: KeytipState): KeytipState => {
'use no memo';

const styles = useStyles();

state.content.className = mergeClasses(
keytipClassNames.content,
styles.root,
state.appearance === 'inverted' && styles.inverted,
state.visible && styles.visible,
state.content.className
);

return state;
};
61 changes: 32 additions & 29 deletions packages/react-keytips/src/components/Keytips/Keytips.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as React from 'react';
import type {
ComponentProps,
ComponentState,
Slot,
EventData,
EventHandler,
} from '@fluentui/react-utilities';
import * as React from 'react';
import { PortalProps } from '@fluentui/react-components';

export type KeytipsSlots = {
root: Slot<'div'>;
Expand All @@ -14,35 +15,37 @@ export type KeytipsSlots = {
type OnExitKeytipsModeData = EventData<'keydown', KeyboardEvent>;
type OnEnterKeytipsModeData = EventData<'keydown', KeyboardEvent>;

export type KeytipsProps = ComponentProps<KeytipsSlots> & {
/**
* Key sequence that will start keytips mode
* @default 'alt+control'
*/
startSequence?: string;
/**
* Key sequences that execute the return functionality in keytips
* (going back to the previous level of keytips)
* @default 'escape'
*/
returnSequence?: string;
/**
* Key sequences that will exit keytips mode
*/
exitSequence?: string;
/**
* Callback function triggered when keytip mode is exited.
*/
onExitKeytipsMode?: EventHandler<OnExitKeytipsModeData>;
/**
* Callback function triggered when keytip mode is entered
*/
onEnterKeytipsMode?: EventHandler<OnEnterKeytipsModeData>;
};
export type KeytipsProps = ComponentProps<KeytipsSlots> &
Pick<PortalProps, 'mountNode'> & {
/**
* Key sequence that will start keytips mode
* @default 'alt+control'
*/
startSequence?: string;
/**
* Key sequences that execute the return functionality in keytips
* (going back to the previous level of keytips)
* @default 'escape'
*/
returnSequence?: string;
/**
* Key sequences that will exit keytips mode
*/
exitSequence?: string;
/**
* Callback function triggered when keytip mode is exited.
*/
onExitKeytipsMode?: EventHandler<OnExitKeytipsModeData>;
/**
* Callback function triggered when keytip mode is entered
*/
onEnterKeytipsMode?: EventHandler<OnEnterKeytipsModeData>;
};

/**
* State used in renderingKeytipsProvider
*/
export type KeytipsState = ComponentState<KeytipsSlots> & {
keytips: React.ReactElement[];
};
export type KeytipsState = ComponentState<KeytipsSlots> &
Pick<KeytipsProps, 'mountNode'> & {
keytips: React.ReactElement[];
};
Loading

0 comments on commit 9e76165

Please sign in to comment.