Skip to content

Commit

Permalink
feat(ui-breadcrumb,ui-tooltip): add tooltips for truncated breadcrumbs
Browse files Browse the repository at this point in the history
Closes: INSTUI-4371
  • Loading branch information
HerrTopi committed Nov 26, 2024
1 parent 71416b3 commit f88ed28
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 29 deletions.
51 changes: 36 additions & 15 deletions packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ import { TruncateText } from '@instructure/ui-truncate-text'
import { Link } from '@instructure/ui-link'
import { omitProps } from '@instructure/ui-react-utils'
import { testable } from '@instructure/ui-testable'
import { Tooltip } from '@instructure/ui-tooltip'

import { propTypes, allowedProps } from './props'
import type { BreadcrumbLinkProps } from './props'
import type { BreadcrumbLinkProps, BreadcrumbLinkState } from './props'

/**
---
Expand All @@ -40,7 +41,10 @@ id: Breadcrumb.Link
**/

@testable()
class BreadcrumbLink extends Component<BreadcrumbLinkProps> {
class BreadcrumbLink extends Component<
BreadcrumbLinkProps,
BreadcrumbLinkState
> {
static readonly componentId = 'Breadcrumb.Link'

static propTypes = propTypes
Expand All @@ -52,26 +56,43 @@ class BreadcrumbLink extends Component<BreadcrumbLinkProps> {
handleRef = (el: Element | null) => {
this.ref = el
}
constructor(props: BreadcrumbLinkProps) {
super(props)

this.state = {
isTruncated: false
}
}
handleTruncation(isTruncated: boolean) {
if (isTruncated !== this.state.isTruncated) {
this.setState({ isTruncated })
}
}
render() {
const { children, href, renderIcon, iconPlacement, onClick, onMouseEnter } =
this.props

const { isTruncated } = this.state
const props = omitProps(this.props, BreadcrumbLink.allowedProps)

return (
<Link
{...props}
href={href}
renderIcon={renderIcon}
iconPlacement={iconPlacement}
onClick={onClick}
onMouseEnter={onMouseEnter}
isWithinText={false}
elementRef={this.handleRef}
>
<TruncateText>{children}</TruncateText>
</Link>
<Tooltip renderTip={children} preventTooltip={!isTruncated}>
<Link
{...props}
href={href}
renderIcon={renderIcon}
iconPlacement={iconPlacement}
onClick={onClick}
onMouseEnter={onMouseEnter}
isWithinText={false}
elementRef={this.handleRef}
>
<TruncateText
onUpdate={(isTruncated) => this.handleTruncation(isTruncated)}
>
<span aria-hidden={isTruncated}>{children}</span>
</TruncateText>
</Link>
</Tooltip>
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,9 @@ const allowedProps: AllowedPropKeys = [
'size'
]

export type { BreadcrumbLinkProps }
type BreadcrumbLinkState = {
isTruncated: boolean
}

export type { BreadcrumbLinkProps, BreadcrumbLinkState }
export { propTypes, allowedProps }
18 changes: 11 additions & 7 deletions packages/ui-tooltip/src/Tooltip/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ class Tooltip extends Component<TooltipProps, TooltipState> {
placement: 'top',
constrain: 'window',
offsetX: 0,
offsetY: 0
offsetY: 0,
preventTooltip: false
} as const

private readonly _id: string
Expand Down Expand Up @@ -148,15 +149,16 @@ class Tooltip extends Component<TooltipProps, TooltipState> {
positionTarget,
onShowContent,
onHideContent,
preventTooltip,
styles,
...rest
} = this.props

return (
<Popover
{...passthroughProps(rest)}
isShowingContent={isShowingContent}
defaultIsShowingContent={defaultIsShowingContent}
isShowingContent={!preventTooltip && isShowingContent}
defaultIsShowingContent={!preventTooltip && defaultIsShowingContent}
on={on}
shouldRenderOffscreen
shouldReturnFocus={false}
Expand All @@ -177,10 +179,12 @@ class Tooltip extends Component<TooltipProps, TooltipState> {
shouldCloseOnDocumentClick={false}
shouldCloseOnEscape
>
<span id={this._id} css={styles?.tooltip} role="tooltip">
{/* TODO: figure out how to add a ref to this */}
{callRenderProp(renderTip)}
</span>
{!preventTooltip ? (
<span id={this._id} css={styles?.tooltip} role="tooltip">
{/* TODO: figure out how to add a ref to this */}
{callRenderProp(renderTip)}
</span>
) : null}
</Popover>
)
}
Expand Down
13 changes: 11 additions & 2 deletions packages/ui-tooltip/src/Tooltip/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ type TooltipOwnProps = {
event: React.UIEvent | React.FocusEvent,
args: { documentClick: boolean }
) => void

/**
* If true, it won't display the tooltip. This is useful in cases when tooltip is conditionally needed
* but in an uncontrolled way
*/
preventTooltip?: boolean
}

type PropKeys = keyof TooltipOwnProps
Expand Down Expand Up @@ -177,6 +183,7 @@ type PropsPassableToPopover = Omit<
| 'onFocus'
| 'onBlur'
| 'elementRef'
| 'preventTooltip'
>

type TooltipProps = PropsPassableToPopover &
Expand Down Expand Up @@ -211,7 +218,8 @@ const propTypes: PropValidators<PropKeys> = {
offsetY: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
positionTarget: PropTypes.oneOfType([element, PropTypes.func]),
onShowContent: PropTypes.func,
onHideContent: PropTypes.func
onHideContent: PropTypes.func,
preventTooltip: PropTypes.bool
}

const allowedProps: AllowedPropKeys = [
Expand All @@ -230,7 +238,8 @@ const allowedProps: AllowedPropKeys = [
'offsetY',
'positionTarget',
'onShowContent',
'onHideContent'
'onHideContent',
'preventTooltip'
]

export type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('<TopNavBarDesktopLayout />', () => {

describe('renderBreadcrumb', () => {
it('should render breadcrumb container', () => {
const { getByLabelText, getByText } = render(
const { getByLabelText, getAllByText } = render(
<TopNavBarContext.Provider
value={{
layout: 'desktop',
Expand All @@ -87,9 +87,9 @@ describe('<TopNavBarDesktopLayout />', () => {
</TopNavBarContext.Provider>
)
const breadCrumbContainer = getByLabelText('You are here')
const crumb1 = getByText('Course page 1')
const crumb2 = getByText('Course page 2')
const crumb3 = getByText('Course page 3')
const crumb1 = getAllByText('Course page 1')[0]
const crumb2 = getAllByText('Course page 2')[0]
const crumb3 = getAllByText('Course page 3')[0]

expect(breadCrumbContainer).toBeInTheDocument()
expect(crumb1).toBeVisible()
Expand Down

0 comments on commit f88ed28

Please sign in to comment.