@konce-pt/react 0.9.0 → 0.10.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/dist/accordion/accordion.js +7 -1
- package/dist/accordion/llms.txt +8 -4
- package/dist/alert/alert.d.ts +10 -4
- package/dist/alert/alert.js +13 -6
- package/dist/alert/llms.txt +8 -6
- package/dist/app-shell/app-shell.js +28 -2
- package/dist/app-shell/llms.txt +3 -0
- package/dist/autocomplete/autocomplete.d.ts +7 -2
- package/dist/autocomplete/autocomplete.js +46 -9
- package/dist/autocomplete/llms.txt +12 -0
- package/dist/avatar/avatar.d.ts +4 -2
- package/dist/avatar/avatar.js +8 -3
- package/dist/avatar/llms.txt +9 -5
- package/dist/avatar-group/avatar-group.d.ts +5 -2
- package/dist/avatar-group/avatar-group.js +10 -2
- package/dist/avatar-group/llms.txt +7 -4
- package/dist/badge/badge.d.ts +8 -1
- package/dist/badge/badge.js +3 -2
- package/dist/badge/llms.txt +11 -5
- package/dist/bottom-sheet/bottom-sheet.d.ts +1 -1
- package/dist/bottom-sheet/bottom-sheet.js +8 -2
- package/dist/bottom-sheet/llms.txt +8 -2
- package/dist/breadcrumb/breadcrumb.js +4 -4
- package/dist/breadcrumb/llms.txt +12 -5
- package/dist/card/card.d.ts +9 -1
- package/dist/card/card.js +2 -2
- package/dist/card/llms.txt +6 -2
- package/dist/carousel/carousel.d.ts +8 -1
- package/dist/carousel/carousel.js +9 -2
- package/dist/carousel/llms.txt +24 -10
- package/dist/checkbox/checkbox.d.ts +6 -1
- package/dist/checkbox/checkbox.js +11 -3
- package/dist/checkbox/llms.txt +11 -0
- package/dist/chip/chip.js +10 -1
- package/dist/chip/llms.txt +7 -4
- package/dist/chips-input/chips-input.d.ts +7 -1
- package/dist/chips-input/chips-input.js +21 -5
- package/dist/chips-input/llms.txt +12 -0
- package/dist/color-picker/color-picker.d.ts +18 -5
- package/dist/color-picker/color-picker.js +48 -14
- package/dist/color-picker/llms.txt +14 -0
- package/dist/confirm/confirm-container.d.ts +5 -1
- package/dist/confirm/confirm-container.js +15 -1
- package/dist/confirm/llms.txt +4 -2
- package/dist/context-menu/context-menu.d.ts +5 -0
- package/dist/context-menu/context-menu.js +48 -4
- package/dist/context-menu/llms.txt +11 -4
- package/dist/date-range/date-range.d.ts +14 -1
- package/dist/date-range/date-range.js +133 -17
- package/dist/date-range/llms.txt +26 -3
- package/dist/datepicker/datepicker.d.ts +10 -1
- package/dist/datepicker/datepicker.js +147 -14
- package/dist/datepicker/llms.txt +27 -3
- package/dist/dialog/dialog.d.ts +4 -0
- package/dist/dialog/dialog.js +7 -1
- package/dist/dialog/llms.txt +2 -0
- package/dist/drawer/drawer.js +7 -2
- package/dist/drawer/llms.txt +10 -5
- package/dist/empty/empty.d.ts +6 -1
- package/dist/empty/empty.js +2 -2
- package/dist/empty/llms.txt +4 -3
- package/dist/fab/llms.txt +2 -1
- package/dist/file-upload/file-upload.d.ts +5 -0
- package/dist/file-upload/file-upload.js +24 -3
- package/dist/file-upload/llms.txt +15 -4
- package/dist/form-field/form-field-context.d.ts +30 -1
- package/dist/form-field/form-field-context.js +27 -0
- package/dist/form-field/form-field.js +16 -2
- package/dist/form-field/llms.txt +10 -1
- package/dist/galleria/galleria.d.ts +6 -1
- package/dist/galleria/galleria.js +40 -1
- package/dist/galleria/llms.txt +17 -8
- package/dist/icon/icon.d.ts +7 -1
- package/dist/icon/icon.js +18 -4
- package/dist/icon/llms.txt +5 -2
- package/dist/image/image.d.ts +5 -0
- package/dist/image/image.js +38 -2
- package/dist/image/llms.txt +24 -17
- package/dist/input/input.d.ts +1 -1
- package/dist/input/input.js +4 -6
- package/dist/input/llms.txt +5 -0
- package/dist/input-mask/input-mask.d.ts +1 -1
- package/dist/input-mask/input-mask.js +8 -6
- package/dist/input-mask/llms.txt +11 -0
- package/dist/input-number/input-number.d.ts +13 -1
- package/dist/input-number/input-number.js +49 -9
- package/dist/input-number/llms.txt +21 -3
- package/dist/input-otp/input-otp.d.ts +7 -2
- package/dist/input-otp/input-otp.js +36 -7
- package/dist/input-otp/llms.txt +14 -0
- package/dist/internal/calendar-focus.d.ts +8 -0
- package/dist/internal/calendar-focus.js +38 -0
- package/dist/internal/option-nav.d.ts +24 -0
- package/dist/internal/option-nav.js +80 -0
- package/dist/internal/roving.d.ts +9 -0
- package/dist/internal/roving.js +43 -0
- package/dist/internal/use-modal-focus.d.ts +10 -0
- package/dist/internal/use-modal-focus.js +57 -0
- package/dist/knob/knob.d.ts +5 -1
- package/dist/knob/knob.js +4 -2
- package/dist/knob/llms.txt +8 -0
- package/dist/listbox/listbox.d.ts +13 -1
- package/dist/listbox/listbox.js +89 -6
- package/dist/listbox/llms.txt +21 -5
- package/dist/megamenu/llms.txt +28 -10
- package/dist/megamenu/megamenu.d.ts +10 -1
- package/dist/megamenu/megamenu.js +116 -5
- package/dist/menu/llms.txt +12 -1
- package/dist/menu/menu-keys.d.ts +15 -0
- package/dist/menu/menu-keys.js +53 -0
- package/dist/menu/menu-rows.js +1 -1
- package/dist/menu/menu.js +52 -4
- package/dist/menubar/llms.txt +24 -7
- package/dist/menubar/menubar.d.ts +8 -1
- package/dist/menubar/menubar.js +92 -6
- package/dist/meter-group/llms.txt +10 -3
- package/dist/meter-group/meter-group.d.ts +9 -2
- package/dist/meter-group/meter-group.js +8 -2
- package/dist/order-list/llms.txt +11 -3
- package/dist/order-list/order-list.d.ts +5 -1
- package/dist/order-list/order-list.js +46 -8
- package/dist/panel/llms.txt +9 -6
- package/dist/panel/panel.js +3 -1
- package/dist/password/llms.txt +8 -0
- package/dist/password/password.d.ts +6 -1
- package/dist/password/password.js +6 -5
- package/dist/pick-list/llms.txt +12 -3
- package/dist/pick-list/pick-list.d.ts +5 -1
- package/dist/pick-list/pick-list.js +52 -5
- package/dist/popover/llms.txt +9 -2
- package/dist/popover/popover.d.ts +9 -3
- package/dist/popover/popover.js +41 -6
- package/dist/public-api.d.ts +6 -6
- package/dist/public-api.js +1 -1
- package/dist/radio-group/llms.txt +11 -0
- package/dist/radio-group/radio-group.d.ts +9 -1
- package/dist/radio-group/radio-group.js +5 -2
- package/dist/rating/llms.txt +18 -5
- package/dist/rating/rating.d.ts +21 -3
- package/dist/rating/rating.js +87 -12
- package/dist/rich-text/llms.txt +20 -0
- package/dist/rich-text/rich-text.d.ts +7 -1
- package/dist/rich-text/rich-text.js +155 -28
- package/dist/scroll-top/llms.txt +5 -1
- package/dist/scroll-top/scroll-top.js +3 -1
- package/dist/select/llms.txt +21 -7
- package/dist/select/select.d.ts +12 -1
- package/dist/select/select.js +110 -38
- package/dist/sidenav/llms.txt +6 -0
- package/dist/skeleton/llms.txt +3 -2
- package/dist/skeleton/skeleton.d.ts +1 -1
- package/dist/skeleton/skeleton.js +2 -2
- package/dist/slider/llms.txt +14 -1
- package/dist/slider/slider.d.ts +11 -2
- package/dist/slider/slider.js +15 -2
- package/dist/speed-dial/llms.txt +6 -2
- package/dist/speed-dial/speed-dial.d.ts +1 -1
- package/dist/speed-dial/speed-dial.js +14 -3
- package/dist/spinner/llms.txt +12 -8
- package/dist/spinner/spinner.d.ts +5 -1
- package/dist/spinner/spinner.js +12 -2
- package/dist/split-button/llms.txt +11 -3
- package/dist/split-button/split-button.d.ts +3 -1
- package/dist/split-button/split-button.js +43 -3
- package/dist/splitter/llms.txt +14 -4
- package/dist/splitter/splitter.d.ts +9 -2
- package/dist/splitter/splitter.js +15 -2
- package/dist/stepper/llms.txt +8 -4
- package/dist/stepper/stepper.d.ts +3 -2
- package/dist/stepper/stepper.js +7 -2
- package/dist/switch/llms.txt +10 -0
- package/dist/switch/switch.d.ts +1 -1
- package/dist/switch/switch.js +4 -2
- package/dist/switch-group/llms.txt +11 -0
- package/dist/switch-group/switch-group.d.ts +5 -1
- package/dist/switch-group/switch-group.js +7 -2
- package/dist/tabs/llms.txt +10 -4
- package/dist/tabs/tabs.d.ts +4 -0
- package/dist/tabs/tabs.js +21 -2
- package/dist/textarea/llms.txt +10 -0
- package/dist/textarea/textarea.d.ts +3 -1
- package/dist/textarea/textarea.js +13 -8
- package/dist/toast/llms.txt +21 -7
- package/dist/toast/toast-container.d.ts +6 -1
- package/dist/toast/toast-container.js +52 -1
- package/dist/toast/toast-store.d.ts +12 -0
- package/dist/toast/toast-store.js +46 -7
- package/dist/tooltip/llms.txt +10 -3
- package/dist/tooltip/tooltip.d.ts +5 -0
- package/dist/tooltip/tooltip.js +80 -7
- package/dist/tree/llms.txt +20 -8
- package/dist/tree/tree-keys.d.ts +27 -0
- package/dist/tree/tree-keys.js +39 -0
- package/dist/tree/tree.d.ts +12 -5
- package/dist/tree/tree.js +75 -29
- package/package.json +12 -12
package/dist/rating/rating.js
CHANGED
|
@@ -1,30 +1,105 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
4
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
5
5
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
6
|
+
import { useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
6
7
|
import { KptIcon } from "../icon/icon.js";
|
|
8
|
+
const fillOf = (star, shown) => shown >= star ? 'full' : shown >= star - 0.5 ? 'half' : 'empty';
|
|
7
9
|
/**
|
|
8
10
|
* Ocena gwiazdkami. Kliknięcie zapalonej gwiazdki zeruje ocenę, więc wystawioną można cofnąć
|
|
9
11
|
* bez osobnego przycisku „wyczyść".
|
|
10
12
|
*
|
|
13
|
+
* Do edycji to grupa radiowa (`role="radiogroup"`, gwiazdki jako `role="radio"`): jeden przystanek
|
|
14
|
+
* Tab, strzałki zmieniają ocenę o krok (w RTL odwrócone w poziomie), `Home`/`End` ustawiają
|
|
15
|
+
* skrajne, `Delete`/`Backspace` zeruje. Z `allowHalf` każda gwiazdka ma dwie połówki — dwa osobne
|
|
16
|
+
* `radio` („3,5 z 5”), a krok strzałek to 0,5. Tylko do odczytu (`readOnly`) to obrazek z nazwą
|
|
17
|
+
* „4,3 z 5” — bez przycisków, które czytnik ogłaszałby jako wyłączone; gwiazdki pokazują wartość
|
|
18
|
+
* zaokrągloną do połówki.
|
|
19
|
+
*
|
|
11
20
|
* @example
|
|
12
|
-
* <KptRating value={score} onValueChange={setScore} count={5} />
|
|
13
|
-
* <KptRating value={
|
|
21
|
+
* <KptRating value={score} onValueChange={setScore} count={5} ariaLabel="Ocena" />
|
|
22
|
+
* <KptRating value={score} onValueChange={setScore} allowHalf ariaLabel="Ocena" />
|
|
23
|
+
* <KptRating value={4.3} readOnly />
|
|
14
24
|
*/
|
|
15
|
-
export function KptRating({ value, defaultValue = 0, onValueChange, count = 5, readOnly = false, disabled = false, onTouch, locale, dictionary, className, ...rest }) {
|
|
16
|
-
const { t } = useKptMessages({ locale, dictionary });
|
|
25
|
+
export function KptRating({ value, defaultValue = 0, onValueChange, count = 5, allowHalf = false, readOnly = false, id, ariaLabel = null, required = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
26
|
+
const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
|
|
27
|
+
const field = useKptFieldA11y(id, 'kpt-rating');
|
|
17
28
|
const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
|
|
18
29
|
const [hover, setHover] = useState(0);
|
|
19
|
-
const
|
|
30
|
+
const groupRef = useRef(null);
|
|
31
|
+
/** Prośba o fokus na przystanek Tab po najbliższym renderze (zmiana oceny klawiszem). */
|
|
32
|
+
const wantFocus = useRef(false);
|
|
33
|
+
const showInvalid = invalid && (touched ?? true);
|
|
34
|
+
const stars = Array.from({ length: count }, (_, index) => index + 1);
|
|
35
|
+
const step = allowHalf ? 0.5 : 1;
|
|
20
36
|
// Podgląd pod kursorem wygrywa z oceną, ale tylko dopóki kursor jest nad gwiazdkami —
|
|
21
37
|
// po `mouseleave` wraca 0 i zapalona zostaje sama wartość.
|
|
22
38
|
const shown = hover || current;
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
/** Przystanek Tab: wybrana wartość, a bez oceny — pierwsza możliwa. */
|
|
40
|
+
const tabStop = current >= step && current <= count ? current : step;
|
|
41
|
+
/** Liczba w zapisie języka strony („3,5” po polsku). */
|
|
42
|
+
const numberFormat = new Intl.NumberFormat(activeLocale, { maximumFractionDigits: 2 });
|
|
43
|
+
const starLabel = (score) => t('rating.star', { value: numberFormat.format(score), max: count });
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!wantFocus.current)
|
|
46
|
+
return;
|
|
47
|
+
wantFocus.current = false;
|
|
48
|
+
groupRef.current?.querySelector(`[data-value="${tabStop}"]`)?.focus();
|
|
49
|
+
});
|
|
50
|
+
const starVisual = (star, fill, children, hidden = false) => (_jsxs("span", { className: "kpt-rating__star", "data-fill": fill, "aria-hidden": hidden || undefined, children: [_jsx(KptIcon, { name: "star" }), _jsx("span", { className: "kpt-rating__fill", "aria-hidden": "true", children: _jsx(KptIcon, { name: "star" }) }), children] }, star));
|
|
51
|
+
if (readOnly) {
|
|
52
|
+
const score = starLabel(current);
|
|
53
|
+
// Gwiazdki pokazują wartość zaokrągloną do połówki (4,3 → 4,5), nazwa — dokładną.
|
|
54
|
+
const display = Math.round(current * 2) / 2;
|
|
55
|
+
return (_jsx("div", { className: cn('kpt-rating-host', className), ...rest, children: _jsx("div", { className: "kpt-rating kpt-rating--readonly", role: "img", id: field.id, "aria-label": ariaLabel ? `${ariaLabel}: ${score}` : score, "aria-describedby": field.describedBy, children: stars.map((star) => starVisual(star, fillOf(star, display), null, true)) }) }));
|
|
56
|
+
}
|
|
57
|
+
const pick = (score) => {
|
|
58
|
+
if (disabled)
|
|
59
|
+
return;
|
|
60
|
+
setCurrent(current === score ? 0 : score);
|
|
61
|
+
};
|
|
62
|
+
const onKeyDown = (event) => {
|
|
63
|
+
if (disabled)
|
|
64
|
+
return;
|
|
65
|
+
const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
|
|
66
|
+
const forward = rtl ? 'ArrowLeft' : 'ArrowRight';
|
|
67
|
+
const back = rtl ? 'ArrowRight' : 'ArrowLeft';
|
|
68
|
+
let next = null;
|
|
69
|
+
if (event.key === forward || event.key === 'ArrowUp')
|
|
70
|
+
next = Math.min(count, Math.max(step, current + step));
|
|
71
|
+
else if (event.key === back || event.key === 'ArrowDown')
|
|
72
|
+
next = Math.max(step, current - step);
|
|
73
|
+
else if (event.key === 'Home')
|
|
74
|
+
next = step;
|
|
75
|
+
else if (event.key === 'End')
|
|
76
|
+
next = count;
|
|
77
|
+
else if (event.key === 'Delete' || event.key === 'Backspace')
|
|
78
|
+
next = 0;
|
|
79
|
+
else if (event.key === ' ' || event.key === 'Enter') {
|
|
80
|
+
// Spacja na gwiazdce wybiera ją (jak radio); ponowna nie zeruje — od tego jest Delete.
|
|
81
|
+
const score = Number(event.target.dataset['value']);
|
|
82
|
+
if (score)
|
|
83
|
+
next = score;
|
|
84
|
+
}
|
|
85
|
+
if (next === null)
|
|
25
86
|
return;
|
|
26
|
-
|
|
27
|
-
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
setCurrent(next);
|
|
89
|
+
wantFocus.current = true;
|
|
90
|
+
};
|
|
91
|
+
/** „Dotknięcie” dopiero przy wyjściu fokusu z całej grupy, nie przy przejściu między gwiazdkami. */
|
|
92
|
+
const onBlur = (event) => {
|
|
93
|
+
const next = event.relatedTarget;
|
|
94
|
+
if (!next || !event.currentTarget.contains(next))
|
|
95
|
+
onTouch?.();
|
|
28
96
|
};
|
|
29
|
-
|
|
97
|
+
/** Pola wyboru jednej gwiazdki: całość albo — z `allowHalf` — dwie połówki. */
|
|
98
|
+
const optionsOf = (star) => allowHalf
|
|
99
|
+
? [
|
|
100
|
+
{ score: star - 0.5, part: 'start' },
|
|
101
|
+
{ score: star, part: 'end' },
|
|
102
|
+
]
|
|
103
|
+
: [{ score: star, part: 'full' }];
|
|
104
|
+
return (_jsx("div", { className: cn('kpt-rating-host', className), ...rest, children: _jsx("div", { ref: groupRef, className: cn('kpt-rating', disabled && 'kpt-rating--disabled'), role: "radiogroup", id: field.id, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, "aria-required": required || undefined, "aria-invalid": showInvalid || undefined, "aria-disabled": disabled || undefined, onKeyDown: onKeyDown, onBlur: onBlur, onMouseLeave: () => setHover(0), children: stars.map((star) => starVisual(star, fillOf(star, shown), optionsOf(star).map((option) => (_jsx("button", { type: "button", role: "radio", className: cn('kpt-rating__hit', `kpt-rating__hit--${option.part}`), "aria-checked": option.score === current, tabIndex: !disabled && option.score === tabStop ? 0 : -1, "aria-disabled": disabled || undefined, "data-value": option.score, "aria-label": starLabel(option.score), onClick: () => pick(option.score), onMouseEnter: () => !disabled && setHover(option.score) }, option.part))))) }) }));
|
|
30
105
|
}
|
package/dist/rich-text/llms.txt
CHANGED
|
@@ -45,6 +45,11 @@ only when the operation actually changed something — otherwise Tab stays nativ
|
|
|
45
45
|
never becomes a keyboard trap (WCAG 2.1.2). In a table cell Tab moves to the next cell; a list
|
|
46
46
|
inside the cell takes precedence.
|
|
47
47
|
|
|
48
|
+
## The toolbar is one Tab stop
|
|
49
|
+
The toolbar is a named `role="toolbar"` ("Formatting", `richText.toolbar`) with a roving tabindex:
|
|
50
|
+
`←`/`→` (mirrored in RTL) and `Home`/`End` move between tools, skipping disabled ones; the stop
|
|
51
|
+
remembers the last tool, so Tab from the toolbar goes straight to the editor.
|
|
52
|
+
|
|
48
53
|
## Tables
|
|
49
54
|
Insert from an 8×8 grid popover. Dragging across cells makes a rectangular selection that grows
|
|
50
55
|
until it fully covers merged cells on its edges. Right-click opens the menu: insert and delete rows
|
|
@@ -69,3 +74,18 @@ Every label comes from the `richText` namespace, including `richText.menu.*` and
|
|
|
69
74
|
Formatting goes through `document.execCommand`. The API is formally deprecated, but it is the only
|
|
70
75
|
route into the browser's native undo stack (Ctrl+Z); replacing it would mean writing our own
|
|
71
76
|
history, that is, a different editor.
|
|
77
|
+
|
|
78
|
+
## Screen reader and keyboard
|
|
79
|
+
`link` opens a small dialog instead of the browser's `prompt()`: a labelled URL field (prefilled
|
|
80
|
+
with the current link), `Enter` inserts, `Escape` returns to the text with the selection restored.
|
|
81
|
+
The table context menu is a `role="menu"` of `menuitem`s: it also opens from the keyboard (the menu
|
|
82
|
+
key or `Shift+F10` in a cell), the arrows and `Home`/`End` move, `Escape`/`Tab` close it, and focus
|
|
83
|
+
goes back to the cell. Colour swatches are named ("Dark grey", `richText.swatches.*`) instead of
|
|
84
|
+
their hex codes. The table size grid is still mouse-only.
|
|
85
|
+
|
|
86
|
+
## Label and description
|
|
87
|
+
`id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
|
|
88
|
+
generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
|
|
89
|
+
`aria-labelledby` pointing at the wrapper's label (unless `ariaLabel` is set), and the hint or error
|
|
90
|
+
is added to `aria-describedby`. New: `id`, `ariaLabel` and `required` (→ `aria-required`) on the
|
|
91
|
+
`role="textbox"` area.
|
|
@@ -13,6 +13,12 @@ export interface KptRichTextProps extends Omit<ComponentPropsWithRef<'div'>, 'on
|
|
|
13
13
|
placeholder?: string;
|
|
14
14
|
/** Maksymalna głębokość zagnieżdżenia list (Tab, „Dodaj wcięcie"). */
|
|
15
15
|
maxListLevel?: number;
|
|
16
|
+
/** Id elementu fokusowalnego (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
|
|
17
|
+
id?: string;
|
|
18
|
+
/** Nazwa dostępna, gdy kontrolki nie opisuje etykieta (`<label for>` albo `KptFormField`). */
|
|
19
|
+
ariaLabel?: string | null;
|
|
20
|
+
/** Oznacza pole jako wymagane atrybutem `aria-required`. */
|
|
21
|
+
required?: boolean;
|
|
16
22
|
disabled?: boolean;
|
|
17
23
|
invalid?: boolean;
|
|
18
24
|
touched?: boolean;
|
|
@@ -35,4 +41,4 @@ export interface KptRichTextProps extends Omit<ComponentPropsWithRef<'div'>, 'on
|
|
|
35
41
|
* <KptRichText value={html} onValueChange={setHtml} />
|
|
36
42
|
* <KptRichText value={html} onValueChange={setHtml} toolbar={[['bold', 'italic'], ['link']]} />
|
|
37
43
|
*/
|
|
38
|
-
export declare function KptRichText({ value, defaultValue, onValueChange, toolbar, placeholder, maxListLevel, disabled, invalid, touched, onTouch, mentions, mentionLimit, onMention, locale, dictionary, className, ...rest }: Readonly<KptRichTextProps>): import("react").JSX.Element;
|
|
44
|
+
export declare function KptRichText({ value, defaultValue, onValueChange, toolbar, placeholder, maxListLevel, id, ariaLabel, required, disabled, invalid, touched, onTouch, mentions, mentionLimit, onMention, locale, dictionary, className, ...rest }: Readonly<KptRichTextProps>): import("react").JSX.Element;
|
|
@@ -11,6 +11,9 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
11
11
|
import { sanitizeHtml, sanitizeUrl } from "./sanitize.js";
|
|
12
12
|
import { canIndentItem, canOutdentItem, selectedItems, shiftItems } from "./lists.js";
|
|
13
13
|
import { CELL_SELECTED, deleteColumn, deleteRow, insertColumn, insertRow, mergeCells, regionBetween, splitCell, tableHtml, toggleHeaderColumn, toggleHeaderRow, } from "./tables.js";
|
|
14
|
+
import { useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
15
|
+
import { nextRovingIndex } from "../internal/roving.js";
|
|
16
|
+
import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
|
|
14
17
|
const ICON = {
|
|
15
18
|
copy: 'copy',
|
|
16
19
|
paste: 'clipboard',
|
|
@@ -49,9 +52,22 @@ const COMMAND = {
|
|
|
49
52
|
alignJustify: 'justifyFull',
|
|
50
53
|
clear: 'removeFormat',
|
|
51
54
|
};
|
|
55
|
+
/** Paleta kolorów z nazwami (`richText.swatches.*`) — sam kod szesnastkowy nic czytnikowi nie mówi. */
|
|
52
56
|
const SWATCHES = [
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
{ color: '#000000', name: 'black' },
|
|
58
|
+
{ color: '#434343', name: 'darkGray' },
|
|
59
|
+
{ color: '#666666', name: 'gray' },
|
|
60
|
+
{ color: '#999999', name: 'midGray' },
|
|
61
|
+
{ color: '#b7b7b7', name: 'silver' },
|
|
62
|
+
{ color: '#cccccc', name: 'lightGray' },
|
|
63
|
+
{ color: '#ffffff', name: 'white' },
|
|
64
|
+
{ color: '#e11d48', name: 'red' },
|
|
65
|
+
{ color: '#ea580c', name: 'orange' },
|
|
66
|
+
{ color: '#ca8a04', name: 'yellow' },
|
|
67
|
+
{ color: '#16a34a', name: 'green' },
|
|
68
|
+
{ color: '#0891b2', name: 'cyan' },
|
|
69
|
+
{ color: '#2563eb', name: 'blue' },
|
|
70
|
+
{ color: '#7c3aed', name: 'violet' },
|
|
55
71
|
];
|
|
56
72
|
const DEFAULT_TOOLBAR = [
|
|
57
73
|
['copy', 'paste', 'pasteText'],
|
|
@@ -86,7 +102,9 @@ function legacyState(cmd) {
|
|
|
86
102
|
* <KptRichText value={html} onValueChange={setHtml} />
|
|
87
103
|
* <KptRichText value={html} onValueChange={setHtml} toolbar={[['bold', 'italic'], ['link']]} />
|
|
88
104
|
*/
|
|
89
|
-
export function KptRichText({ value, defaultValue = '', onValueChange, toolbar = DEFAULT_TOOLBAR, placeholder = '', maxListLevel = 6, disabled = false, invalid = false, touched, onTouch, mentions = [], mentionLimit = KPT_MENTION_LIMIT, onMention, locale, dictionary, className, ...rest }) {
|
|
105
|
+
export function KptRichText({ value, defaultValue = '', onValueChange, toolbar = DEFAULT_TOOLBAR, placeholder = '', maxListLevel = 6, id, ariaLabel = null, required = false, disabled = false, invalid = false, touched, onTouch, mentions = [], mentionLimit = KPT_MENTION_LIMIT, onMention, locale, dictionary, className, ...rest }) {
|
|
106
|
+
/** `contenteditable` nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
|
|
107
|
+
const field = useKptFieldA11y(id, 'kpt-rich-text');
|
|
90
108
|
const { t } = useKptMessages({ locale, dictionary });
|
|
91
109
|
const container = useKptOverlayContainer();
|
|
92
110
|
const editorRef = useRef(null);
|
|
@@ -98,7 +116,15 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
98
116
|
const [foreAnchor, setForeAnchor] = useState(null);
|
|
99
117
|
const [backAnchor, setBackAnchor] = useState(null);
|
|
100
118
|
const [tableAnchor, setTableAnchor] = useState(null);
|
|
119
|
+
const [linkAnchor, setLinkAnchor] = useState(null);
|
|
120
|
+
/** Okienko adresu linku (zamiast `window.prompt()`) i wpisywany adres. */
|
|
121
|
+
const [linkOpen, setLinkOpen] = useState(false);
|
|
122
|
+
const [linkDraft, setLinkDraft] = useState('');
|
|
123
|
+
/** Menu tabeli otwarte z klawiatury — po zamontowaniu fokus idzie na pierwszą pozycję. */
|
|
124
|
+
const menuFromKeyboard = useRef(false);
|
|
101
125
|
const [styleOpen, setStyleOpen] = useState(false);
|
|
126
|
+
/** Narzędzie, na którym ostatnio stał fokus paska — przystanek Tab przy powrocie. */
|
|
127
|
+
const [lastTool, setLastTool] = useState(null);
|
|
102
128
|
const [colorOpen, setColorOpen] = useState(false);
|
|
103
129
|
const [colorKind, setColorKind] = useState('foreColor');
|
|
104
130
|
const [tableOpen, setTableOpen] = useState(false);
|
|
@@ -408,15 +434,9 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
408
434
|
}
|
|
409
435
|
focusEditor();
|
|
410
436
|
switch (item) {
|
|
411
|
-
case 'link':
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const safe = sanitizeUrl(url);
|
|
415
|
-
if (safe)
|
|
416
|
-
legacyExec('createLink', safe);
|
|
417
|
-
}
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
437
|
+
case 'link':
|
|
438
|
+
openLink();
|
|
439
|
+
return;
|
|
420
440
|
case 'copy':
|
|
421
441
|
void copySelection();
|
|
422
442
|
return;
|
|
@@ -482,6 +502,10 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
482
502
|
// co innego niż w samym edytorze.
|
|
483
503
|
if (mentionOpen && onMentionKeyDown(event))
|
|
484
504
|
return;
|
|
505
|
+
if ((event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10')) && openMenuFromKeyboard()) {
|
|
506
|
+
event.preventDefault();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
485
509
|
if (event.key !== 'Tab' || event.ctrlKey || event.altKey || event.metaKey)
|
|
486
510
|
return;
|
|
487
511
|
if (shiftItems(editorRef.current, event.shiftKey ? 'outdent' : 'indent', maxListLevel)) {
|
|
@@ -556,9 +580,67 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
556
580
|
pushValue();
|
|
557
581
|
};
|
|
558
582
|
const afterTableOp = () => {
|
|
583
|
+
const fromMenu = !!document.activeElement?.closest('.kpt-rt-menu');
|
|
559
584
|
setMenuOpen(false);
|
|
585
|
+
// Pozycja wybrana z klawiatury znika razem z menu — fokus wraca do edytora, nie na <body>.
|
|
586
|
+
if (fromMenu)
|
|
587
|
+
focusEditor();
|
|
560
588
|
pushValue();
|
|
561
589
|
};
|
|
590
|
+
/** Zamknięcie menu z powrotem fokusu (i kursora) do komórki, z której je otwarto. */
|
|
591
|
+
const closeMenuToEditor = () => {
|
|
592
|
+
setMenuOpen(false);
|
|
593
|
+
focusEditor();
|
|
594
|
+
restoreSelection();
|
|
595
|
+
};
|
|
596
|
+
/** Klawiatura menu kontekstowego: strzałki, `Home`/`End`; `Escape`/`Tab` zamykają i wracają do tekstu. */
|
|
597
|
+
const onMenuKeyDown = (event) => {
|
|
598
|
+
if (handleMenuKeydown(event.currentTarget, event.nativeEvent) === 'close')
|
|
599
|
+
closeMenuToEditor();
|
|
600
|
+
};
|
|
601
|
+
/** Menu tabeli z klawiatury: klawisz menu albo `Shift+F10` w komórce — jak natywne menu kontekstowe. */
|
|
602
|
+
const openMenuFromKeyboard = () => {
|
|
603
|
+
const node = window.getSelection()?.anchorNode ?? null;
|
|
604
|
+
const el = node instanceof Element ? node : (node?.parentElement ?? null);
|
|
605
|
+
const cell = el?.closest('td, th');
|
|
606
|
+
if (!cell || !editorRef.current?.contains(cell))
|
|
607
|
+
return false;
|
|
608
|
+
const rect = cell.getBoundingClientRect();
|
|
609
|
+
saveSelection();
|
|
610
|
+
ctxCell.current = cell;
|
|
611
|
+
canSplitCtx.current = (cell.colSpan || 1) > 1 || (cell.rowSpan || 1) > 1;
|
|
612
|
+
setMenuPoint({ x: rect.left, y: rect.bottom });
|
|
613
|
+
menuFromKeyboard.current = true;
|
|
614
|
+
setMenuOpen(true);
|
|
615
|
+
return true;
|
|
616
|
+
};
|
|
617
|
+
/** Okienko adresu zamiast `window.prompt()` — nazwane pole, `Enter` wstawia, `Escape` wraca do tekstu. */
|
|
618
|
+
const openLink = () => {
|
|
619
|
+
if (disabled)
|
|
620
|
+
return;
|
|
621
|
+
saveSelection();
|
|
622
|
+
const node = savedRange.current?.commonAncestorContainer ?? null;
|
|
623
|
+
const el = node instanceof Element ? node : (node?.parentElement ?? null);
|
|
624
|
+
setLinkDraft(el?.closest('a')?.getAttribute('href') ?? '');
|
|
625
|
+
setLinkOpen(true);
|
|
626
|
+
};
|
|
627
|
+
const applyLink = () => {
|
|
628
|
+
const safe = sanitizeUrl(linkDraft.trim());
|
|
629
|
+
setLinkOpen(false);
|
|
630
|
+
focusEditor();
|
|
631
|
+
restoreSelection();
|
|
632
|
+
if (safe) {
|
|
633
|
+
legacyExec('createLink', safe);
|
|
634
|
+
pushValue();
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
const closeLink = (refocus) => {
|
|
638
|
+
setLinkOpen(false);
|
|
639
|
+
if (refocus) {
|
|
640
|
+
focusEditor();
|
|
641
|
+
restoreSelection();
|
|
642
|
+
}
|
|
643
|
+
};
|
|
562
644
|
const onContextMenu = (event) => {
|
|
563
645
|
if (disabled)
|
|
564
646
|
return;
|
|
@@ -566,6 +648,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
566
648
|
if (!cell || !editorRef.current?.contains(cell))
|
|
567
649
|
return; // poza tabelą zostaje natywne menu
|
|
568
650
|
event.preventDefault();
|
|
651
|
+
saveSelection();
|
|
569
652
|
ctxCell.current = cell;
|
|
570
653
|
canSplitCtx.current = (cell.colSpan || 1) > 1 || (cell.rowSpan || 1) > 1;
|
|
571
654
|
setMenuPoint({ x: event.clientX, y: event.clientY });
|
|
@@ -655,7 +738,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
655
738
|
return canOutdent;
|
|
656
739
|
return true;
|
|
657
740
|
};
|
|
658
|
-
const palette = (onPick, customLabel) => (_jsxs("div", { className: "kpt-rich-text__palette", onMouseDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "kpt-rich-text__swatches", children: SWATCHES.map((
|
|
741
|
+
const palette = (onPick, customLabel) => (_jsxs("div", { className: "kpt-rich-text__palette", onMouseDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "kpt-rich-text__swatches", children: SWATCHES.map((swatch) => (_jsx("button", { type: "button", className: "kpt-rich-text__swatch", style: { background: swatch.color }, "aria-label": t(`richText.swatches.${swatch.name}`), onClick: () => onPick(swatch.color) }, swatch.color))) }), _jsxs("label", { className: "kpt-rich-text__picker", children: [_jsx("span", { children: customLabel }), _jsx("input", { type: "color", onInput: (event) => onPick(event.target.value) })] })] }));
|
|
659
742
|
/**
|
|
660
743
|
* Panel otwarty przy dolnej albo prawej krawędzi okna wychodziłby poza ekran — CDK robił to
|
|
661
744
|
* w Angularze sam, tutaj przesuwamy go po zamontowaniu, gdy już znać jego rozmiar.
|
|
@@ -683,11 +766,35 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
683
766
|
onClose();
|
|
684
767
|
} }), _jsx("div", { ref: clampToViewport, style: { position: 'fixed', left: menuPoint.x, top: menuPoint.y, ...layer }, children: content })] }), container);
|
|
685
768
|
};
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
769
|
+
/**
|
|
770
|
+
* Przystanek Tab paska: ostatnio użyte narzędzie, a gdy jest wyłączone albo go nie ma —
|
|
771
|
+
* pierwsze dostępne. Dzięki temu `Tab` z paska idzie od razu do edytora, a nie przez kilkanaście
|
|
772
|
+
* przycisków.
|
|
773
|
+
*/
|
|
774
|
+
const usableTools = sections.flat().filter((item) => !disabled && enabled(item));
|
|
775
|
+
const toolStop = lastTool && usableTools.includes(lastTool) ? lastTool : (usableTools[0] ?? null);
|
|
776
|
+
const rovingProps = (item) => ({ 'data-tool': item, tabIndex: item === toolStop ? 0 : -1 });
|
|
777
|
+
/** Strzałki po pasku (w RTL odwrócone), `Home`/`End` — skrajne; wyłączone narzędzia są pomijane. */
|
|
778
|
+
const onToolbarKeyDown = (event) => {
|
|
779
|
+
const bar = event.currentTarget;
|
|
780
|
+
const buttons = [...bar.querySelectorAll('[data-tool]')];
|
|
781
|
+
const current = buttons.indexOf(event.target);
|
|
782
|
+
if (current < 0)
|
|
783
|
+
return;
|
|
784
|
+
const rtl = getComputedStyle(bar).direction === 'rtl';
|
|
785
|
+
const key = rtl && event.key === 'ArrowLeft' ? 'ArrowRight' : rtl && event.key === 'ArrowRight' ? 'ArrowLeft' : event.key;
|
|
786
|
+
const next = nextRovingIndex(buttons.map((button) => button.disabled), current, key);
|
|
787
|
+
if (next === null)
|
|
788
|
+
return;
|
|
789
|
+
event.preventDefault();
|
|
790
|
+
setLastTool(buttons[next].dataset['tool']);
|
|
791
|
+
buttons[next].focus();
|
|
792
|
+
};
|
|
793
|
+
const toolButton = (item) => (_jsx("button", { ...rovingProps(item), ref: item === 'link' ? setLinkAnchor : undefined, "aria-haspopup": item === 'link' ? 'dialog' : undefined, "aria-expanded": item === 'link' ? linkOpen : undefined, type: "button", className: cn('kpt-rich-text__tool', isActive(item) && 'is-active'), "aria-label": t(`richText.${item}`), "aria-pressed": pressed(item), title: t(`richText.${item}`), disabled: disabled || !enabled(item), onMouseDown: (event) => event.preventDefault(), onClick: () => exec(item), children: _jsx(KptIcon, { name: ICON[item] }) }, item));
|
|
794
|
+
const menuItem = (icon, label, onClick, extra) => (_jsxs("button", { type: "button", role: "menuitem", tabIndex: -1, className: cn('kpt-rt-menu__item', extra), "aria-disabled": extra === 'is-disabled' || undefined, onClick: onClick, children: [_jsx(KptIcon, { name: icon }), " ", label] }));
|
|
795
|
+
return (_jsxs("div", { className: cn('kpt-rich-text-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-rich-text', showInvalid && 'kpt-rich-text--invalid', disabled && 'kpt-rich-text--disabled'), children: [_jsx("div", { className: "kpt-rich-text__toolbar", role: "toolbar", "aria-label": t('richText.toolbar'), "aria-controls": field.id, onKeyDown: onToolbarKeyDown, children: sections.map((section, index) => (_jsxs("span", { style: { display: 'contents' }, children: [index > 0 ? _jsx("span", { className: "kpt-rich-text__divider", "aria-hidden": "true" }) : null, section.map((item) => {
|
|
689
796
|
if (item === 'heading') {
|
|
690
|
-
return (_jsxs("button", { ref: setStyleAnchor, type: "button", className: cn('kpt-rich-text__style', styleOpen && 'is-open'), disabled: disabled, "aria-label": t('richText.heading'), "aria-expanded": styleOpen, title: t('richText.heading'), onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
797
|
+
return (_jsxs("button", { ref: setStyleAnchor, ...rovingProps(item), type: "button", className: cn('kpt-rich-text__style', styleOpen && 'is-open'), disabled: disabled, "aria-label": t('richText.heading'), "aria-expanded": styleOpen, title: t('richText.heading'), onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
691
798
|
if (disabled)
|
|
692
799
|
return;
|
|
693
800
|
saveSelection();
|
|
@@ -695,7 +802,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
695
802
|
}, children: [_jsx("span", { children: styleLabel }), _jsx(KptIcon, { name: "chevron-down", className: "kpt-rich-text__style-caret" })] }, item));
|
|
696
803
|
}
|
|
697
804
|
if (item === 'foreColor' || item === 'backColor') {
|
|
698
|
-
return (_jsx("button", { ref: item === 'foreColor' ? setForeAnchor : setBackAnchor, type: "button", className: "kpt-rich-text__tool kpt-rich-text__tool--color", "aria-label": t(`richText.${item}`), title: t(`richText.${item}`), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
805
|
+
return (_jsx("button", { ref: item === 'foreColor' ? setForeAnchor : setBackAnchor, ...rovingProps(item), type: "button", className: "kpt-rich-text__tool kpt-rich-text__tool--color", "aria-label": t(`richText.${item}`), title: t(`richText.${item}`), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
699
806
|
if (disabled)
|
|
700
807
|
return;
|
|
701
808
|
saveSelection();
|
|
@@ -704,10 +811,10 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
704
811
|
}, children: _jsx(KptIcon, { name: ICON[item] }) }, item));
|
|
705
812
|
}
|
|
706
813
|
if (item === 'table') {
|
|
707
|
-
return (_jsx("button", { ref: setTableAnchor, type: "button", className: cn('kpt-rich-text__tool', tableOpen && 'is-active'), "aria-label": t('richText.table'), title: t('richText.table'), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => exec(item), children: _jsx(KptIcon, { name: ICON[item] }) }, item));
|
|
814
|
+
return (_jsx("button", { ref: setTableAnchor, ...rovingProps(item), type: "button", className: cn('kpt-rich-text__tool', tableOpen && 'is-active'), "aria-label": t('richText.table'), title: t('richText.table'), disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => exec(item), children: _jsx(KptIcon, { name: ICON[item] }) }, item));
|
|
708
815
|
}
|
|
709
816
|
return toolButton(item);
|
|
710
|
-
})] }, index))) }), sourceView ? (_jsx("textarea", { className: "kpt-rich-text__source", value: current, disabled: disabled, spellCheck: false, "aria-label": t('richText.htmlSource'), onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() })) : (_jsx("div", { ref: editorRef, className: "kpt-rich-text__editor", contentEditable: !disabled, suppressContentEditableWarning: true, "data-placeholder": placeholder, "aria-invalid": showInvalid || undefined, role: "textbox", "aria-multiline": "true", onInput: () => {
|
|
817
|
+
})] }, index))) }), sourceView ? (_jsx("textarea", { className: "kpt-rich-text__source", value: current, disabled: disabled, spellCheck: false, "aria-label": t('richText.htmlSource'), onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() })) : (_jsx("div", { ref: editorRef, className: "kpt-rich-text__editor", contentEditable: !disabled, suppressContentEditableWarning: true, "data-placeholder": placeholder, "aria-invalid": showInvalid || undefined, role: "textbox", "aria-multiline": "true", id: field.id, "aria-required": required || undefined, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, onInput: () => {
|
|
711
818
|
pushValue();
|
|
712
819
|
syncMention();
|
|
713
820
|
}, onKeyDown: onKeyDown, onPaste: onPaste, onContextMenu: onContextMenu, onMouseDown: onEditorMouseDown, onMouseMove: onEditorMouseMove, onBlur: () => onTouch?.() }))] }), mentionOpen && container
|
|
@@ -722,7 +829,22 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
722
829
|
event.preventDefault();
|
|
723
830
|
insertMention(item);
|
|
724
831
|
}, onMouseEnter: () => setMentionActive(index), children: [_jsx("span", { className: "kpt-rt-mentions__label", children: item.label }), item.kind ? _jsx("span", { className: "kpt-rt-mentions__kind", children: item.kind }) : null] }, item.id))) }), container)
|
|
725
|
-
: null, _jsx(KptOverlay, { open:
|
|
832
|
+
: null, _jsx(KptOverlay, { open: linkOpen, anchor: linkAnchor, onClose: () => closeLink(false), offset: 4, children: _jsxs("form", { className: "kpt-rt-link", role: "dialog",
|
|
833
|
+
// Bez walidacji przeglądarki: „example.com” jest poprawnym wpisem — `sanitizeUrl` dopisze https://.
|
|
834
|
+
noValidate: true, "aria-label": t('richText.link'), onSubmit: (event) => {
|
|
835
|
+
event.preventDefault();
|
|
836
|
+
applyLink();
|
|
837
|
+
}, onKeyDown: (event) => {
|
|
838
|
+
if (event.key !== 'Escape')
|
|
839
|
+
return;
|
|
840
|
+
event.preventDefault();
|
|
841
|
+
event.stopPropagation();
|
|
842
|
+
closeLink(true);
|
|
843
|
+
}, children: [_jsx("label", { className: "kpt-rt-link__label", htmlFor: `${field.id}-link`, children: t('richText.linkPrompt') }), _jsx("input", { ref: (el) => {
|
|
844
|
+
// Panel jest niewidoczny do pierwszego pomiaru — fokus po klatce.
|
|
845
|
+
if (el)
|
|
846
|
+
requestAnimationFrame(() => el.focus());
|
|
847
|
+
}, className: "kpt-rt-link__input", id: `${field.id}-link`, type: "url", inputMode: "url", placeholder: "https://", value: linkDraft, onChange: (event) => setLinkDraft(event.target.value) }), _jsx("button", { type: "submit", className: "kpt-rt-link__apply", children: t('richText.linkApply') })] }) }), _jsx(KptOverlay, { open: styleOpen, anchor: styleAnchor, onClose: () => setStyleOpen(false), offset: 4, children: _jsx("ul", { className: "kpt-rich-text__style-panel", role: "listbox", children: styleOptions.map((option) => (_jsx("li", { className: cn(`kpt-rich-text__style-opt kpt-rich-text__style-opt--${option.tag}`, currentBlock === option.tag && 'is-selected'), role: "option", "aria-selected": currentBlock === option.tag, onClick: () => {
|
|
726
848
|
setStyleOpen(false);
|
|
727
849
|
focusEditor();
|
|
728
850
|
restoreSelection();
|
|
@@ -733,7 +855,12 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
733
855
|
setTableOpen(false);
|
|
734
856
|
setGrid({ r: 0, c: 0 });
|
|
735
857
|
}, offset: 4, children: _jsxs("div", { className: "kpt-rt-grid-pop", onMouseDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "kpt-rt-grid", onMouseLeave: () => setGrid({ r: 0, c: 0 }), children: GRID_ROWS.map((r) => (_jsx("div", { className: "kpt-rt-grid__row", children: GRID_COLS.map((c) => (_jsx("span", { className: cn('kpt-rt-grid__cell', r <= grid.r && c <= grid.c && 'is-on'), onMouseEnter: () => setGrid({ r, c }), onClick: () => insertTableAt(r, c) }, c))) }, r))) }), _jsxs("div", { className: "kpt-rt-grid__label", children: [grid.r, " \u00D7 ", grid.c] })] }) }), menuOpen
|
|
736
|
-
? pointPanel(_jsxs("div", {
|
|
858
|
+
? pointPanel(_jsxs("div", { ref: (el) => {
|
|
859
|
+
if (el && menuFromKeyboard.current) {
|
|
860
|
+
menuFromKeyboard.current = false;
|
|
861
|
+
requestAnimationFrame(() => focusMenuEdge(el, 'first'));
|
|
862
|
+
}
|
|
863
|
+
}, className: "kpt-rt-menu", role: "menu", "aria-label": t('richText.table'), onContextMenu: (event) => event.preventDefault(), onKeyDown: onMenuKeyDown, children: [_jsx("div", { className: "kpt-rt-menu__group", "aria-hidden": "true", children: t('richText.menu.column') }), menuItem('plus', t('richText.menu.insertLeft'), () => {
|
|
737
864
|
if (ctxCell.current)
|
|
738
865
|
insertColumn(ctxCell.current, 'left');
|
|
739
866
|
afterTableOp();
|
|
@@ -750,7 +877,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
750
877
|
if (ctxCell.current)
|
|
751
878
|
deleteColumn(ctxCell.current);
|
|
752
879
|
afterTableOp();
|
|
753
|
-
}), _jsx("div", { className: "kpt-rt-menu__sep" }), _jsx("div", { className: "kpt-rt-menu__group", children: t('richText.menu.row') }), menuItem('plus', t('richText.menu.insertAbove'), () => {
|
|
880
|
+
}), _jsx("div", { className: "kpt-rt-menu__sep", role: "separator" }), _jsx("div", { className: "kpt-rt-menu__group", "aria-hidden": "true", children: t('richText.menu.row') }), menuItem('plus', t('richText.menu.insertAbove'), () => {
|
|
754
881
|
if (ctxCell.current)
|
|
755
882
|
insertRow(ctxCell.current, 'above');
|
|
756
883
|
afterTableOp();
|
|
@@ -767,7 +894,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
767
894
|
if (ctxCell.current)
|
|
768
895
|
deleteRow(ctxCell.current);
|
|
769
896
|
afterTableOp();
|
|
770
|
-
}), _jsx("div", { className: "kpt-rt-menu__sep" }), _jsx("div", { className: "kpt-rt-menu__group", children: t('richText.menu.cell') }), menuItem('palette', t('richText.menu.cellBackground'), () => {
|
|
897
|
+
}), _jsx("div", { className: "kpt-rt-menu__sep", role: "separator" }), _jsx("div", { className: "kpt-rt-menu__group", "aria-hidden": "true", children: t('richText.menu.cell') }), menuItem('palette', t('richText.menu.cellBackground'), () => {
|
|
771
898
|
setMenuOpen(false);
|
|
772
899
|
setColorKind('cellBg');
|
|
773
900
|
setColorOpen(true);
|
|
@@ -783,7 +910,7 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
783
910
|
for (const cell of activeCells())
|
|
784
911
|
cell.style.textAlign = 'right';
|
|
785
912
|
afterTableOp();
|
|
786
|
-
}), _jsx("div", { className: "kpt-rt-menu__sep" }), menuItem('table', t('richText.menu.mergeCells'), () => {
|
|
913
|
+
}), _jsx("div", { className: "kpt-rt-menu__sep", role: "separator" }), menuItem('table', t('richText.menu.mergeCells'), () => {
|
|
787
914
|
mergeCells([...selCells.current]);
|
|
788
915
|
clearCellSelection();
|
|
789
916
|
afterTableOp();
|
|
@@ -791,15 +918,15 @@ export function KptRichText({ value, defaultValue = '', onValueChange, toolbar =
|
|
|
791
918
|
if (ctxCell.current)
|
|
792
919
|
splitCell(ctxCell.current);
|
|
793
920
|
afterTableOp();
|
|
794
|
-
}, canSplitCtx.current ? undefined : 'is-disabled'), _jsx("div", { className: "kpt-rt-menu__sep" }), menuItem('table', t('richText.menu.border'), () => {
|
|
921
|
+
}, canSplitCtx.current ? undefined : 'is-disabled'), _jsx("div", { className: "kpt-rt-menu__sep", role: "separator" }), menuItem('table', t('richText.menu.border'), () => {
|
|
795
922
|
setMenuOpen(false);
|
|
796
923
|
setBorderOpen(true);
|
|
797
|
-
}), _jsx("div", { className: "kpt-rt-menu__sep" }), menuItem('trash', t('richText.menu.deleteTable'), () => {
|
|
924
|
+
}), _jsx("div", { className: "kpt-rt-menu__sep", role: "separator" }), menuItem('trash', t('richText.menu.deleteTable'), () => {
|
|
798
925
|
ctxCell.current?.closest('table')?.remove();
|
|
799
926
|
afterTableOp();
|
|
800
927
|
}, 'kpt-rt-menu__item--danger')] }), () => setMenuOpen(false))
|
|
801
928
|
: null, borderOpen
|
|
802
|
-
? pointPanel(_jsxs("div", { className: "kpt-rt-border", onMouseDown: (event) => event.preventDefault(), children: [_jsxs("div", { className: "kpt-rt-border__seg", children: [_jsx("button", { type: "button", className: cn(borderTarget === 'table' && 'is-active'), onClick: () => setBorderTarget('table'), children: t('richText.border.table') }), _jsx("button", { type: "button", className: cn(borderTarget === 'cell' && 'is-active'), onClick: () => setBorderTarget('cell'), children: t('richText.border.cell') })] }), _jsxs("div", { className: "kpt-rt-border__widths", children: [_jsx("button", { type: "button", onClick: () => setBorderWidth('none'), children: t('richText.border.none') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('thin'), children: t('richText.border.thin') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('medium'), children: t('richText.border.medium') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('thick'), children: t('richText.border.thick') })] }), _jsx("div", { className: "kpt-rich-text__swatches", children: SWATCHES.map((
|
|
929
|
+
? pointPanel(_jsxs("div", { className: "kpt-rt-border", onMouseDown: (event) => event.preventDefault(), children: [_jsxs("div", { className: "kpt-rt-border__seg", children: [_jsx("button", { type: "button", className: cn(borderTarget === 'table' && 'is-active'), onClick: () => setBorderTarget('table'), children: t('richText.border.table') }), _jsx("button", { type: "button", className: cn(borderTarget === 'cell' && 'is-active'), onClick: () => setBorderTarget('cell'), children: t('richText.border.cell') })] }), _jsxs("div", { className: "kpt-rt-border__widths", children: [_jsx("button", { type: "button", onClick: () => setBorderWidth('none'), children: t('richText.border.none') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('thin'), children: t('richText.border.thin') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('medium'), children: t('richText.border.medium') }), _jsx("button", { type: "button", onClick: () => setBorderWidth('thick'), children: t('richText.border.thick') })] }), _jsx("div", { className: "kpt-rich-text__swatches", children: SWATCHES.map((swatch) => (_jsx("button", { type: "button", className: "kpt-rich-text__swatch", style: { background: swatch.color }, "aria-label": t(`richText.swatches.${swatch.name}`), onClick: () => setBorderColor(swatch.color) }, swatch.color))) }), _jsxs("label", { className: "kpt-rich-text__picker", children: [_jsx("span", { children: t('richText.border.customColor') }), _jsx("input", { type: "color", onInput: (event) => setBorderColor(event.target.value) })] })] }), () => setBorderOpen(false))
|
|
803
930
|
: null, colorOpen && colorKind === 'cellBg'
|
|
804
931
|
? pointPanel(palette(applyColor, t('richText.paletteCustom')), () => setColorOpen(false))
|
|
805
932
|
: null] }));
|
package/dist/scroll-top/llms.txt
CHANGED
|
@@ -38,4 +38,8 @@ The label comes from `scrollTop.label`.
|
|
|
38
38
|
`--kpt-color-primary` / `--kpt-color-on-primary`, `--kpt-elevation-3`.
|
|
39
39
|
|
|
40
40
|
## Accessibility
|
|
41
|
-
A real `<button>`
|
|
41
|
+
A real `<button>` labelled from `scrollTop.label`; while hidden it is `visibility: hidden`, so it is
|
|
42
|
+
out of the tab order and the accessibility tree. It sits in the bottom-end corner
|
|
43
|
+
(`inset-inline-end`), so it moves to the left in RTL. With `prefers-reduced-motion` the scroll is a
|
|
44
|
+
jump instead of smooth and the button appears without a transition. Focus stays on the button after
|
|
45
|
+
the jump — move it to your page heading if that matters.
|
|
@@ -29,7 +29,9 @@ export function KptScrollTop({ threshold = 300, target = null, locale, dictionar
|
|
|
29
29
|
const scroller = target
|
|
30
30
|
? (document.querySelector(target) ?? window)
|
|
31
31
|
: window;
|
|
32
|
-
|
|
32
|
+
// Przy `prefers-reduced-motion` skok zamiast płynnego przewijania.
|
|
33
|
+
const behavior = matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
|
|
34
|
+
scroller.scrollTo({ top: 0, behavior });
|
|
33
35
|
};
|
|
34
36
|
return (_jsx("div", { className: cn('kpt-scroll-top-host', className), ...rest, children: _jsx("button", { type: "button", className: cn('kpt-scroll-top', visible && 'is-visible'),
|
|
35
37
|
// Ukryty przycisk zostaje w DOM (animacja pojawiania), więc z kolejności Tab wyjmuje go
|
package/dist/select/llms.txt
CHANGED
|
@@ -18,7 +18,8 @@ A select on its own panel. Variants: single, with a search box (`filter`) and mu
|
|
|
18
18
|
- `value` / `defaultValue` / `onValueChange` — a single `T`, or `T[]` in `multiple` mode
|
|
19
19
|
- `open` / `defaultOpen` / `onOpenChange` — the panel state, so the list can be opened from outside
|
|
20
20
|
- `placeholder`, `multiple`, `filter`, `disabled`, `invalid`, `touched`
|
|
21
|
-
- `onTouch()` — fires when the panel closes or the trigger
|
|
21
|
+
- `onTouch()` — fires when the panel closes or focus leaves the closed trigger
|
|
22
|
+
- `aria-label` (through the rest props) names the trigger when no `<label for>` describes it
|
|
22
23
|
- `locale`, `dictionary` — i18n overrides for this component only
|
|
23
24
|
- `className`, `ref` and every other `<div>` prop go to the trigger
|
|
24
25
|
|
|
@@ -29,9 +30,14 @@ flips above when there is no room, a transparent backdrop that closes on click,
|
|
|
29
30
|
follows scrolling and resizing, so the panel never drifts away from its trigger.
|
|
30
31
|
|
|
31
32
|
## Keyboard
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
The select-only combobox pattern: focus stays on the trigger and the active option is announced
|
|
34
|
+
through `aria-activedescendant` (with `filter`, focus moves to the search field). Closed: ArrowDown,
|
|
35
|
+
ArrowUp, Enter, Space, Home and End open the list, typing letters opens it on the first match, and
|
|
36
|
+
in `multiple` mode Backspace/Delete removes the last chip. Open: ArrowUp/ArrowDown move by one,
|
|
37
|
+
Home/End to the ends, PageUp/PageDown by ten, skipping disabled options without wrapping; Enter (and
|
|
38
|
+
Space on the trigger) picks — a single select closes and returns focus to the trigger; typing jumps
|
|
39
|
+
to the next match, diacritics ignored; Escape and Tab close. Hovering an option makes it active, so
|
|
40
|
+
the mouse and the keyboard share one highlight, and clicking it does not take focus from the trigger.
|
|
35
41
|
|
|
36
42
|
## Inside KptFormField
|
|
37
43
|
There is no native field for the wrapper to read, so the select reports its filled state with
|
|
@@ -46,11 +52,19 @@ There is no native field for the wrapper to read, so the select reports its fill
|
|
|
46
52
|
<KptSelect options={roles} value={role} onValueChange={setRole} filter />
|
|
47
53
|
<KptSelect options={tags} value={tags} onValueChange={setTags} multiple filter />
|
|
48
54
|
|
|
55
|
+
## Label and description
|
|
56
|
+
`id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
|
|
57
|
+
generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
|
|
58
|
+
`aria-labelledby` pointing at the wrapper's label (unless `aria-label` is set), and the hint or
|
|
59
|
+
error is added to `aria-describedby`. New: `id` and `required` (→ `aria-required`); the list gets
|
|
60
|
+
the same name as the trigger.
|
|
61
|
+
|
|
49
62
|
## Tokens
|
|
50
63
|
`--kpt-form-field-{bg,text,placeholder,border,border-hover,border-focus,border-error,radius}`,
|
|
51
64
|
`--kpt-color-surface-raised` and `--kpt-elevation-3` for the panel, `--kpt-z-overlay` for the layer.
|
|
52
65
|
|
|
53
66
|
## Accessibility
|
|
54
|
-
The trigger is `role="combobox"` with `aria-
|
|
55
|
-
`role="listbox"` with `aria-multiselectable`, and each
|
|
56
|
-
`aria-selected`.
|
|
67
|
+
The trigger is `role="combobox"` with `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls`
|
|
68
|
+
and `aria-activedescendant`; the list is `role="listbox"` with `aria-multiselectable`, and each
|
|
69
|
+
option is `role="option"` with `aria-selected` and `aria-disabled`. Chip remove buttons sit outside
|
|
70
|
+
the Tab order and are named with the option label.
|