From 9e76165c69edaf05fac121458cbfbad0cb62ac44 Mon Sep 17 00:00:00 2001 From: mainframev Date: Wed, 28 Aug 2024 22:35:48 +0200 Subject: [PATCH] fixup! chore(react-keytips): draft implementation --- packages/react-keytips/package.json | 4 +- .../src/components/Keytip/Keytip.tsx | 3 + .../src/components/Keytip/Keytip.types.ts | 61 ++++++++++++--- .../src/components/Keytip/renderKeytip.tsx | 19 ++--- .../src/components/Keytip/useKeytip.tsx | 77 +++++++++++++++---- .../Keytip/useKeytipStyles.styles.ts | 61 +++++++++++++++ .../src/components/Keytips/Keytips.types.ts | 61 ++++++++------- .../src/components/Keytips/renderKeytips.tsx | 15 +++- .../src/components/Keytips/useKeytips.tsx | 1 + 9 files changed, 232 insertions(+), 70 deletions(-) create mode 100644 packages/react-keytips/src/components/Keytip/useKeytipStyles.styles.ts diff --git a/packages/react-keytips/package.json b/packages/react-keytips/package.json index 0d7a5649..76ca202f 100644 --- a/packages/react-keytips/package.json +++ b/packages/react-keytips/package.json @@ -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", diff --git a/packages/react-keytips/src/components/Keytip/Keytip.tsx b/packages/react-keytips/src/components/Keytip/Keytip.tsx index 07a991a4..e16e209c 100644 --- a/packages/react-keytips/src/components/Keytip/Keytip.tsx +++ b/packages/react-keytips/src/components/Keytip/Keytip.tsx @@ -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, @@ -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); }; diff --git a/packages/react-keytips/src/components/Keytip/Keytip.types.ts b/packages/react-keytips/src/components/Keytip/Keytip.types.ts index f01df963..dba4ccab 100644 --- a/packages/react-keytips/src/components/Keytip/Keytip.types.ts +++ b/packages/react-keytips/src/components/Keytip/Keytip.types.ts @@ -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>; +}; export type ExecuteKeytipEventHandler = EventHandler< EventData<'keydown', KeyboardEvent> & { @@ -14,15 +28,34 @@ export type ReturnKeytipEventHandler = 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 & { /** - * 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. */ @@ -45,6 +78,12 @@ export type KeytipProps = Pick< export type KeytipWithId = KeytipProps & { uniqueId: string }; -export type KeytipState = Required< - Pick ->; +export type KeytipState = ComponentState & + Required< + Pick + > & { + /** + * Whether the Keytip should be rendered to the DOM. + */ + shouldRenderTooltip?: boolean; + }; diff --git a/packages/react-keytips/src/components/Keytip/renderKeytip.tsx b/packages/react-keytips/src/components/Keytip/renderKeytip.tsx index 61d57971..95a0da2e 100644 --- a/packages/react-keytips/src/components/Keytip/renderKeytip.tsx +++ b/packages/react-keytips/src/components/Keytip/renderKeytip.tsx @@ -1,8 +1,8 @@ /** @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'; @@ -10,15 +10,8 @@ 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 ( - - ); + assertSlots(state); + return state.shouldRenderTooltip ? ( + {state.content.children} + ) : null; }; diff --git a/packages/react-keytips/src/components/Keytip/useKeytip.tsx b/packages/react-keytips/src/components/Keytip/useKeytip.tsx index 45c2c236..01a44ff0 100644 --- a/packages/react-keytips/src/components/Keytip/useKeytip.tsx +++ b/packages/react-keytips/src/components/Keytip/useKeytip.tsx @@ -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. * @@ -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; + containerRef: React.MutableRefObject; + } = 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; }; diff --git a/packages/react-keytips/src/components/Keytip/useKeytipStyles.styles.ts b/packages/react-keytips/src/components/Keytip/useKeytipStyles.styles.ts new file mode 100644 index 00000000..0cad261b --- /dev/null +++ b/packages/react-keytips/src/components/Keytip/useKeytipStyles.styles.ts @@ -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 = { + 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; +}; diff --git a/packages/react-keytips/src/components/Keytips/Keytips.types.ts b/packages/react-keytips/src/components/Keytips/Keytips.types.ts index d2d351f6..80b6da7f 100644 --- a/packages/react-keytips/src/components/Keytips/Keytips.types.ts +++ b/packages/react-keytips/src/components/Keytips/Keytips.types.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import type { ComponentProps, ComponentState, @@ -5,7 +6,7 @@ import type { EventData, EventHandler, } from '@fluentui/react-utilities'; -import * as React from 'react'; +import { PortalProps } from '@fluentui/react-components'; export type KeytipsSlots = { root: Slot<'div'>; @@ -14,35 +15,37 @@ export type KeytipsSlots = { type OnExitKeytipsModeData = EventData<'keydown', KeyboardEvent>; type OnEnterKeytipsModeData = EventData<'keydown', KeyboardEvent>; -export type KeytipsProps = ComponentProps & { - /** - * 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; - /** - * Callback function triggered when keytip mode is entered - */ - onEnterKeytipsMode?: EventHandler; -}; +export type KeytipsProps = ComponentProps & + Pick & { + /** + * 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; + /** + * Callback function triggered when keytip mode is entered + */ + onEnterKeytipsMode?: EventHandler; + }; /** * State used in renderingKeytipsProvider */ -export type KeytipsState = ComponentState & { - keytips: React.ReactElement[]; -}; +export type KeytipsState = ComponentState & + Pick & { + keytips: React.ReactElement[]; + }; diff --git a/packages/react-keytips/src/components/Keytips/renderKeytips.tsx b/packages/react-keytips/src/components/Keytips/renderKeytips.tsx index 09e4fb35..481270ff 100644 --- a/packages/react-keytips/src/components/Keytips/renderKeytips.tsx +++ b/packages/react-keytips/src/components/Keytips/renderKeytips.tsx @@ -5,11 +5,24 @@ import { createElement } from '@fluentui/react-jsx-runtime'; import { assertSlots } from '@fluentui/react-utilities'; import type { KeytipsState, KeytipsSlots } from './Keytips.types'; +import { Portal, makeStyles } from '@fluentui/react-components'; /** * Render the final JSX of Keytips */ +const useStyles = makeStyles({ + portal: { + // Ensure the keytips are above everything else + zIndex: 1000001, + }, +}); + export const renderKeytips_unstable = (state: KeytipsState) => { assertSlots(state); + const classes = useStyles(); - return {state.keytips}; + return ( + + {state.keytips} + + ); }; diff --git a/packages/react-keytips/src/components/Keytips/useKeytips.tsx b/packages/react-keytips/src/components/Keytips/useKeytips.tsx index 6844155d..be3cadef 100644 --- a/packages/react-keytips/src/components/Keytips/useKeytips.tsx +++ b/packages/react-keytips/src/components/Keytips/useKeytips.tsx @@ -260,6 +260,7 @@ export const useKeytips_unstable = (props: KeytipsProps): KeytipsState => { components: { root: 'div', }, + mountNode: props.mountNode, keytips: keytipsToRender, root: slot.always( getIntrinsicElementProps('div', {