@scalewing/react 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/components/Tooltip.d.ts +17 -4
- package/dist/components/Tooltip.js +4 -5
- package/dist/components/tooltip-trigger-aria.d.ts +12 -0
- package/dist/components/tooltip-trigger-aria.js +23 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- cfcac1a: `Tooltip` `relationship` (`docs/requests/teisoro-tooltip-labels.md`, Teisoro's workspace navigation). `relationship?: 'description' | 'label'`, default `'description'`, and the exported type `TooltipRelationship`. `'label'` makes the tooltip its trigger's accessible name: the trigger gets `aria-labelledby` with the tooltip's id (after its own, if any) instead of `aria-describedby`, so an icon-only control's name is read once rather than as both name and description. The tooltip stays in the page, hidden, so it names the trigger while not shown. With `disabled` there is no tooltip and no reference to it; the trigger then needs its own name (visible text or `aria-label`). Without `relationship` the markup and behavior are unchanged. No new classes, tokens or dependencies.
|
|
8
|
+
|
|
3
9
|
## 1.19.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -247,7 +247,7 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
247
247
|
|
|
248
248
|
`Progress` shows a known value between zero and a positive maximum. It uses native progressbar semantics and displays the value and maximum beside the localized label. Invalid bounds throw instead of silently clamping. Optional `tone` is `accent`, `success`, or `danger`. `showCount={false}` hides the visible `value / max` count when the page shows its own count caption, so the count appears once; the progress bar still exposes its value and maximum.
|
|
249
249
|
|
|
250
|
-
`Tooltip` adds supplementary plain-text help to one labelled, focusable trigger. Supply localized `content` and an existing trigger element
|
|
250
|
+
`Tooltip` adds supplementary plain-text help to one labelled, focusable trigger, or names an icon-only one (`relationship="label"`, below). Supply localized `content` and an existing trigger element; by default it has its own accessible name. It opens on hover or focus, closes on pointer leave, blur, Escape, or outside touch, and toggles on touch. Keep required instructions visible outside the tooltip. `disabled` turns the tooltip off, not its trigger: the trigger stays mounted, enabled and focusable, and there is no tooltip, no `aria-describedby` from it, and Escape is left to the page. Use it when what the tooltip says is visible at some widths, such as text shown only from `md` up, instead of rendering the trigger with and without a `Tooltip`, which mounts a new trigger and drops its focus. Focus, hover and touch are still followed, so a trigger that still has focus, the pointer or an open touch toggle shows the tooltip as soon as it is enabled again (Escape pressed while disabled reaches the page and does not stop that). `disabled` must be the same in the server render and the first client render, or hydration mismatches; read a breakpoint through a hydration-safe store, such as `useSyncExternalStore` with a server snapshot. `relationship="label"` makes the tooltip its trigger's name instead of its description, for an icon-only control whose tooltip says what it is: the trigger gets `aria-labelledby` (after its own, if any) and no `aria-describedby` from the tooltip, so the text is read once. The tooltip stays in the page, hidden, so it names the trigger while it is not shown, and it wins over an `aria-label`. While `disabled` there is no tooltip to name the trigger, so give the trigger its own name for that state: visible text, such as a label shown only from `md` up, or an `aria-label`. Do not use `relationship="label"` while the trigger shows visible text that differs from `content`: the tooltip would replace that visible label as the name (WCAG 2.5.3, label in name). The default, `relationship="description"`, is the supplementary help above. `relationship` must also be the same in the server render and the first client render.
|
|
251
251
|
|
|
252
252
|
`Separator` divides sections with a token-colored line. It is horizontal by default; use `orientation="vertical"` in a flex row. Set `decorative` when the line only supports layout so assistive technology ignores it.
|
|
253
253
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ReactElement } from 'react';
|
|
2
|
+
import { type TooltipRelationship, type TooltipTriggerAria } from './tooltip-trigger-aria.js';
|
|
3
|
+
export type { TooltipRelationship };
|
|
2
4
|
export type TooltipProps = {
|
|
3
5
|
content: string;
|
|
4
6
|
/**
|
|
@@ -11,8 +13,19 @@ export type TooltipProps = {
|
|
|
11
13
|
* render and the first client render.
|
|
12
14
|
*/
|
|
13
15
|
disabled?: boolean;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* `'description'` (default) adds supplementary help to a trigger that has
|
|
18
|
+
* its own name, through `aria-describedby`. `'label'` makes `content` the
|
|
19
|
+
* trigger's accessible name, through `aria-labelledby`, for an icon-only
|
|
20
|
+
* control: the text is read once, as its name. The tooltip stays in the
|
|
21
|
+
* DOM, hidden, so it names the trigger while it is not shown. While
|
|
22
|
+
* `disabled` there is no tooltip to name it, so the trigger then needs its
|
|
23
|
+
* own name: visible text, or an `aria-label` (which the tooltip overrides
|
|
24
|
+
* while enabled). The trigger's own `aria-labelledby` and `aria-describedby`
|
|
25
|
+
* are kept. It must match between the server render and the first client
|
|
26
|
+
* render.
|
|
27
|
+
*/
|
|
28
|
+
relationship?: TooltipRelationship;
|
|
29
|
+
trigger: ReactElement<TooltipTriggerAria>;
|
|
17
30
|
};
|
|
18
|
-
export declare function Tooltip({ content, disabled, trigger }: TooltipProps): import("react").JSX.Element;
|
|
31
|
+
export declare function Tooltip({ content, disabled, relationship, trigger, }: TooltipProps): import("react").JSX.Element;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { cloneElement, useEffect, useId, useRef, useState, } from 'react';
|
|
4
|
-
|
|
4
|
+
import { tooltipTriggerAria, } from './tooltip-trigger-aria.js';
|
|
5
|
+
export function Tooltip({ content, disabled = false, relationship = 'description', trigger, }) {
|
|
5
6
|
if (!content.trim())
|
|
6
7
|
throw new RangeError('content must not be empty');
|
|
7
8
|
// `open` follows focus, hover and touch even while disabled; `shown` is
|
|
@@ -10,9 +11,7 @@ export function Tooltip({ content, disabled = false, trigger }) {
|
|
|
10
11
|
const shown = open && !disabled;
|
|
11
12
|
const id = useId();
|
|
12
13
|
const wrapperRef = useRef(null);
|
|
13
|
-
const
|
|
14
|
-
.filter(Boolean)
|
|
15
|
-
.join(' ') || undefined;
|
|
14
|
+
const triggerAria = tooltipTriggerAria(trigger.props, disabled ? undefined : id, relationship);
|
|
16
15
|
useEffect(() => {
|
|
17
16
|
if (!open)
|
|
18
17
|
return;
|
|
@@ -43,5 +42,5 @@ export function Tooltip({ content, disabled = false, trigger }) {
|
|
|
43
42
|
}, onPointerDownCapture: (event) => {
|
|
44
43
|
if (event.pointerType === 'touch')
|
|
45
44
|
setOpen((current) => !current);
|
|
46
|
-
}, ref: wrapperRef, children: [cloneElement(trigger,
|
|
45
|
+
}, ref: wrapperRef, children: [cloneElement(trigger, triggerAria), disabled ? null : (_jsx("span", { className: "sw-tooltip", hidden: !shown, id: id, role: "tooltip", children: content }))] }));
|
|
47
46
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a tooltip is to its trigger. A `description` supplements a trigger
|
|
3
|
+
* that already has a name; a `label` is the trigger's name, for an icon-only
|
|
4
|
+
* control whose tooltip says what it is.
|
|
5
|
+
*/
|
|
6
|
+
export type TooltipRelationship = 'description' | 'label';
|
|
7
|
+
/** The trigger's own references, kept and joined with the tooltip's id. */
|
|
8
|
+
export type TooltipTriggerAria = {
|
|
9
|
+
'aria-describedby'?: string;
|
|
10
|
+
'aria-labelledby'?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function tooltipTriggerAria(own: TooltipTriggerAria, tooltipId: string | undefined, relationship: TooltipRelationship): TooltipTriggerAria;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A description tooltip adds its id to the trigger's own `aria-describedby`
|
|
3
|
+
* and leaves its `aria-labelledby` exactly as it was. A label tooltip adds
|
|
4
|
+
* its id to the trigger's own `aria-labelledby` instead and leaves its
|
|
5
|
+
* `aria-describedby` as it was, so its text is the trigger's name (it wins
|
|
6
|
+
* over an `aria-label`) and is read once, not as both name and description.
|
|
7
|
+
* A disabled tooltip is not rendered (`tooltipId` undefined), so the trigger
|
|
8
|
+
* never points at a missing element.
|
|
9
|
+
*/
|
|
10
|
+
export function tooltipTriggerAria(own, tooltipId, relationship) {
|
|
11
|
+
if (relationship === 'label')
|
|
12
|
+
return {
|
|
13
|
+
'aria-describedby': own['aria-describedby'],
|
|
14
|
+
'aria-labelledby': joinIdRefs(own['aria-labelledby'], tooltipId),
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
'aria-describedby': joinIdRefs(own['aria-describedby'], tooltipId),
|
|
18
|
+
'aria-labelledby': own['aria-labelledby'],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function joinIdRefs(...ids) {
|
|
22
|
+
return ids.filter(Boolean).join(' ') || undefined;
|
|
23
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { CalendarButton, type CalendarButtonLabels, type CalendarButtonProps, }
|
|
|
8
8
|
export { Checkbox, type CheckboxProps } from './components/Checkbox.js';
|
|
9
9
|
export { Spinner, type SpinnerProps, type SpinnerSize, } from './components/Spinner.js';
|
|
10
10
|
export { Progress, type ProgressProps, type ProgressTone, } from './components/Progress.js';
|
|
11
|
-
export { Tooltip, type TooltipProps } from './components/Tooltip.js';
|
|
11
|
+
export { Tooltip, type TooltipProps, type TooltipRelationship, } from './components/Tooltip.js';
|
|
12
12
|
export { RadioGroup, type RadioGroupOption, type RadioGroupProps, } from './components/RadioGroup.js';
|
|
13
13
|
export { AppHeader, type AppHeaderProps } from './components/AppHeader.js';
|
|
14
14
|
export { Badge, type BadgeProps, type BadgeSize, type BadgeTone, badgeSizes, badgeTones, } from './components/Badge.js';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { CalendarButton, } from './components/CalendarButton.js';
|
|
|
7
7
|
export { Checkbox } from './components/Checkbox.js';
|
|
8
8
|
export { Spinner, } from './components/Spinner.js';
|
|
9
9
|
export { Progress, } from './components/Progress.js';
|
|
10
|
-
export { Tooltip } from './components/Tooltip.js';
|
|
10
|
+
export { Tooltip, } from './components/Tooltip.js';
|
|
11
11
|
export { RadioGroup, } from './components/RadioGroup.js';
|
|
12
12
|
export { AppHeader } from './components/AppHeader.js';
|
|
13
13
|
export { Badge, badgeSizes, badgeTones, } from './components/Badge.js';
|