jira/apps/web/core/components/common/access-field.tsx
Anmol Singh Bhatia fd68e7d919
refactor: migrate web @plane/propel Tooltip to @makeplane/propel (#9726)
* refactor: migrate web @plane/propel Tooltip to @makeplane/propel

Move all 144 Tooltip call sites in apps/web from the in-repo
@plane/propel/tooltip to the published
@makeplane/propel/components/tooltip.

Prop mapping applied on every site:
- tooltipContent -> label (string)
- tooltipHeading -> folded into label + layout="stacked"
- position -> side/align (top/center omitted; it is the published default)
- isMobile -> disabled (OR'd with any existing disabled)
- renderByDefault -> dropped; the in-repo Tooltip declared but never
  used it
- className -> dropped, or mapped to sideOffset/alignOffset (3 sites)

layout is "single" for short bounded copy and "stacked" wherever the
label interpolates unbounded data (names, titles, URLs, joined lists) —
"single" is whitespace-nowrap, so long labels would overflow.

Behavior change: tooltips now open after the published 600ms default
instead of the in-repo 200ms. delay is deliberately not set per site.

ReactNode content:
- Editor toolbars map name + <kbd>shortcut</kbd> onto the published
  shortcut prop
- <br/> and fragment copy folds into one wrapping stacked string
- stickies/action-bar rendered a full <StickyNote> hover preview, not a
  tooltip; it moves to PreviewCard/PreviewCardTrigger/PreviewCardContent

Published label is a string, so DropdownButtonProps.tooltipContent,
date-range's customTooltipContent, and the two rich-filters components
narrow from ReactNode to string. rich-filters/root.tsx guards
filterConfig.tooltipContent, which stays ReactNode in @plane/types.

Folded labels use ?? "" where the source expression is nullable so they
cannot render the string "undefined" where the in-repo tooltip rendered
nothing.

Zero @plane/propel/tooltip references remain under apps/web.

* fix: address review findings on the Tooltip migration

Accessibility (CodeRabbit): Base UI wires no aria-describedby or
aria-labelledby between the tooltip trigger and its popup — verified by
rendering the published Tooltip and inspecting the DOM, the trigger comes
out as `<button data-base-ui-tooltip-trigger>` with no aria at all and the
popup has role="tooltip" but no id. So a `label` never reaches assistive
tech, and the four icon-only triggers announced as bare "button". Add an
explicit aria-label at each: extended-project-sidebar, layout-switcher,
layout-selection, and access-field. Pre-existing gap (the in-repo tooltip
sat on the same unwired Base UI trigger), fixed here since the PR touches
these sites.

AccessField now requires i18n_label and drops the optional pre-i18n
`label`, so the computed label is non-empty by construction and serves as
both the tooltip copy and the accessible name. Its only caller already
passes i18n_label for every specifier, which also retires the TODO.

Correctness: inbox issue-properties interpolated a nullable name into a
template literal, so a duplicate work item with no name rendered the
literal "undefined"; pass the name with ?? "" instead.

CodeQL useless conditional: drop `|| ""` from stickyId in action-bar —
both call sites already sit behind a guard proving recentStickyId truthy,
and StickyNote treats "" and undefined identically.

React Doctor prefer-module-scope-static-value: hoist badgeContent to
module scope as BADGE_CONTENT; it holds only static strings.

React Doctor js-combine-iterations: collapse the filter/map chains to a
single pass. The two layout switchers skip inside map instead of
pre-filtering; LabelSummary reduces project labels into the selected
names once rather than filter -> map -> join.
2026-09-01 17:15:12 +05:30

56 lines
1.8 KiB
TypeScript

/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { LucideIcon } from "lucide-react";
// plane ui
import { useTranslation } from "@plane/i18n";
import type { ISvgIcons } from "@plane/propel/icons";
import { Tooltip } from "@makeplane/propel/components/tooltip";
// plane utils
import { cn } from "@plane/utils";
type Props = {
onChange: (value: number) => void;
value: number;
accessSpecifiers: {
key: number;
i18n_label: string;
icon: LucideIcon | React.FC<ISvgIcons>;
}[];
isMobile?: boolean;
};
export function AccessField(props: Props) {
const { onChange, value, accessSpecifiers, isMobile = false } = props;
const { t } = useTranslation();
return (
<div className="flex flex-shrink-0 items-stretch gap-0.5 rounded-sm border-[1px] border-subtle p-1">
{accessSpecifiers.map((access, index) => {
const label = t(access.i18n_label);
return (
<Tooltip key={access.key} label={label} layout="stacked" disabled={isMobile}>
<button
type="button"
aria-label={label}
onClick={() => onChange(access.key)}
className={cn(
"relative flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-xs p-1 transition-all",
value === access.key ? "bg-layer-1" : "hover:bg-layer-1"
)}
tabIndex={2 + index}
>
<access.icon
className={cn("h-3.5 w-3.5 transition-all", value === access.key ? "text-primary" : "text-placeholder")}
strokeWidth={2}
/>
</button>
</Tooltip>
);
})}
</div>
);
}