@konce-pt/react 0.9.1 → 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
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { cn } from "../internal/cn.js";
|
|
3
4
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
'#
|
|
10
|
-
'#
|
|
11
|
-
'#
|
|
12
|
-
'#
|
|
13
|
-
'#
|
|
14
|
-
|
|
5
|
+
import { useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
6
|
+
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
7
|
+
const DEFAULT_PRESETS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899', '#111827', '#ffffff'];
|
|
8
|
+
/** Nazwy domyślnej palety (klucze `colorPicker.*`). */
|
|
9
|
+
const DEFAULT_NAMES = {
|
|
10
|
+
'#ef4444': 'red',
|
|
11
|
+
'#f59e0b': 'amber',
|
|
12
|
+
'#10b981': 'green',
|
|
13
|
+
'#3b82f6': 'blue',
|
|
14
|
+
'#8b5cf6': 'violet',
|
|
15
|
+
'#ec4899': 'pink',
|
|
16
|
+
'#111827': 'black',
|
|
17
|
+
'#ffffff': 'white',
|
|
18
|
+
};
|
|
19
|
+
const HEX = /^#[0-9a-f]{6}$/i;
|
|
15
20
|
/**
|
|
16
21
|
* Wybór koloru: próbka, pole hex i presety. Wartością jest hex.
|
|
17
22
|
*
|
|
@@ -21,8 +26,37 @@ const DEFAULT_PRESETS = [
|
|
|
21
26
|
* @example
|
|
22
27
|
* <KptColorPicker value={color} onValueChange={setColor} />
|
|
23
28
|
*/
|
|
24
|
-
export function KptColorPicker({ value, defaultValue = '#3b82f6', onValueChange, presets = DEFAULT_PRESETS, disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
|
|
25
|
-
const
|
|
29
|
+
export function KptColorPicker({ value, defaultValue = '#3b82f6', onValueChange, presets = DEFAULT_PRESETS, id, ariaLabel = null, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
30
|
+
const { t } = useKptMessages({ locale, dictionary });
|
|
31
|
+
const field = useKptFieldA11y(id, 'kpt-color-picker');
|
|
32
|
+
/** Tekst w polu hex, dopóki nie jest poprawnym kodem — wartość zmienia się dopiero na `#rrggbb`. */
|
|
33
|
+
const [hexDraft, setHexDraft] = useState(null);
|
|
34
|
+
const hexInvalid = hexDraft !== null && !HEX.test(hexDraft);
|
|
35
|
+
const presetList = presets.map((preset) => {
|
|
36
|
+
const item = typeof preset === 'string' ? { value: preset, label: '' } : preset;
|
|
37
|
+
const named = DEFAULT_NAMES[item.value.toLowerCase()];
|
|
38
|
+
return { value: item.value, label: item.label || (named ? t(`colorPicker.${named}`) : item.value) };
|
|
39
|
+
});
|
|
40
|
+
const isActive = (color) => color.toLowerCase() === current.toLowerCase();
|
|
26
41
|
const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
|
|
27
|
-
return (_jsx("div", { className: cn('kpt-color-picker-host', className), ...rest, children: _jsxs("div", { className: "kpt-color-picker", children: [_jsx("label", { className: "kpt-color-picker__swatch", style: { backgroundColor: current }, children: _jsx("input", { type: "color", value: current, disabled: disabled, onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() }) }), _jsx("input", { type: "text", className: "kpt-color-picker__hex", id: field
|
|
42
|
+
return (_jsx("div", { className: cn('kpt-color-picker-host', className), ...rest, children: _jsxs("div", { className: "kpt-color-picker", children: [_jsx("label", { className: "kpt-color-picker__swatch", style: { backgroundColor: current }, children: _jsx("input", { type: "color", value: current, disabled: disabled, "aria-label": t('colorPicker.picker'), onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() }) }), _jsx("input", { type: "text", className: "kpt-color-picker__hex", id: field.id, "aria-label": field.labelledBy ? undefined : (ariaLabel ?? t('colorPicker.hex')), "aria-describedby": field.describedBy, value: hexDraft ?? current, disabled: disabled, maxLength: 7, "aria-invalid": (invalid && (touched ?? true)) || hexInvalid || undefined, onChange: (event) => {
|
|
43
|
+
let text = event.target.value.trim();
|
|
44
|
+
if (text && !text.startsWith('#'))
|
|
45
|
+
text = `#${text}`;
|
|
46
|
+
if (HEX.test(text)) {
|
|
47
|
+
setHexDraft(null);
|
|
48
|
+
setCurrent(text.toLowerCase());
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
setHexDraft(text);
|
|
52
|
+
}
|
|
53
|
+
}, onBlur: () => {
|
|
54
|
+
// Wyjście z pola z niepełnym kodem zostawia go widocznym i oznaczonym `aria-invalid`.
|
|
55
|
+
if (hexDraft === '')
|
|
56
|
+
setHexDraft(null);
|
|
57
|
+
onTouch?.();
|
|
58
|
+
} }), _jsx("div", { className: "kpt-color-picker__presets", children: presetList.map((preset) => (_jsx("button", { type: "button", className: cn('kpt-color-picker__preset', isActive(preset.value) && 'is-active'), style: { backgroundColor: preset.value }, disabled: disabled, "aria-pressed": isActive(preset.value), onClick: () => {
|
|
59
|
+
setHexDraft(null);
|
|
60
|
+
setCurrent(preset.value);
|
|
61
|
+
}, "aria-label": preset.label }, preset.value))) })] }) }));
|
|
28
62
|
}
|
|
@@ -31,6 +31,20 @@ same value as text, and the presets write it directly.
|
|
|
31
31
|
<KptColorPicker value={color} onValueChange={setColor} />
|
|
32
32
|
<KptColorPicker value={color} onValueChange={setColor} presets={['#ef4444', '#3b82f6']} />
|
|
33
33
|
|
|
34
|
+
## Screen reader and keyboard
|
|
35
|
+
`presets` accepts codes or `{ value, label }` (the `KptColorPreset` type): the label names the
|
|
36
|
+
swatch, because a hex code says little to a screen reader; the default palette is named from i18n
|
|
37
|
+
(`colorPicker.red`…). The active swatch has `aria-pressed`. The hex field changes the value only on
|
|
38
|
+
a full `#rrggbb` code (a missing `#` is added); an incomplete code stays visible with
|
|
39
|
+
`aria-invalid`.
|
|
40
|
+
|
|
41
|
+
## Label and description
|
|
42
|
+
`id` goes to the native field, not the host; without one it comes from `KptFormField` or is
|
|
43
|
+
generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
|
|
44
|
+
added to `aria-describedby` (merged with your own). Without a wrapper, name it with `ariaLabel`.
|
|
45
|
+
`id` and the label go to the hex field; the native colour well is named "Pick a colour"
|
|
46
|
+
(`colorPicker.picker`), and the hex field falls back to "Hex code" (`colorPicker.hex`).
|
|
47
|
+
|
|
34
48
|
## Tokens
|
|
35
49
|
The border `--kpt-form-field-border`, the field `--kpt-form-field-{bg,text,radius}`, the active
|
|
36
50
|
preset `--kpt-color-focus-ring`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentPropsWithRef } from 'react';
|
|
2
2
|
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
3
3
|
export interface KptConfirmContainerProps extends ComponentPropsWithRef<'div'>, KptI18nOverrides {
|
|
4
4
|
}
|
|
@@ -6,6 +6,10 @@ export interface KptConfirmContainerProps extends ComponentPropsWithRef<'div'>,
|
|
|
6
6
|
* Kontener potwierdzeń — renderuje żądanie z `kptConfirm()` na `KptDialog`. Umieść raz
|
|
7
7
|
* w aplikacji, obok `<KptToastContainer />`.
|
|
8
8
|
*
|
|
9
|
+
* Okno to `role="alertdialog"` z treścią pytania w `aria-describedby`, więc czytnik odczytuje
|
|
10
|
+
* je od razu po otwarciu. Fokus startuje na przycisku odmowy — bezpieczniejszej odpowiedzi,
|
|
11
|
+
* której przypadkowy `Enter` niczego nie usunie.
|
|
12
|
+
*
|
|
9
13
|
* @example
|
|
10
14
|
* <KptConfirmContainer />
|
|
11
15
|
* // gdziekolwiek: if (await kptConfirm({ message: 'Usunąć?' })) { … }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
2
3
|
import { cn } from "../internal/cn.js";
|
|
4
|
+
import { useKptId } from "../internal/use-id.js";
|
|
3
5
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
4
6
|
import { KptButton } from "../button/button.js";
|
|
5
7
|
import { KptDialog } from "../dialog/dialog.js";
|
|
@@ -9,6 +11,10 @@ import { acceptKptConfirm, rejectKptConfirm, useKptConfirmRequest } from "./conf
|
|
|
9
11
|
* Kontener potwierdzeń — renderuje żądanie z `kptConfirm()` na `KptDialog`. Umieść raz
|
|
10
12
|
* w aplikacji, obok `<KptToastContainer />`.
|
|
11
13
|
*
|
|
14
|
+
* Okno to `role="alertdialog"` z treścią pytania w `aria-describedby`, więc czytnik odczytuje
|
|
15
|
+
* je od razu po otwarciu. Fokus startuje na przycisku odmowy — bezpieczniejszej odpowiedzi,
|
|
16
|
+
* której przypadkowy `Enter` niczego nie usunie.
|
|
17
|
+
*
|
|
12
18
|
* @example
|
|
13
19
|
* <KptConfirmContainer />
|
|
14
20
|
* // gdziekolwiek: if (await kptConfirm({ message: 'Usunąć?' })) { … }
|
|
@@ -16,11 +22,19 @@ import { acceptKptConfirm, rejectKptConfirm, useKptConfirmRequest } from "./conf
|
|
|
16
22
|
export function KptConfirmContainer({ locale, dictionary, className, ...rest }) {
|
|
17
23
|
const { t } = useKptMessages({ locale, dictionary });
|
|
18
24
|
const request = useKptConfirmRequest();
|
|
25
|
+
const messageId = useKptId(undefined, 'kpt-confirm-message');
|
|
26
|
+
const rejectRef = useRef(null);
|
|
27
|
+
// `showModal()` skupia pierwszy element okna, czyli „×” w nagłówku. Efekt dziecka (`KptDialog`)
|
|
28
|
+
// biegnie przed efektem rodzica, więc tu okno jest już otwarte i fokus można przenieść.
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (request)
|
|
31
|
+
rejectRef.current?.focus();
|
|
32
|
+
}, [request]);
|
|
19
33
|
return (_jsx("div", { className: cn('kpt-confirm-container-host', className), ...rest, children: _jsxs(KptDialog, { open: request !== null,
|
|
20
34
|
// Escape i × zamykają okno bez odpowiedzi — traktujemy to jak odmowę, żeby czekający
|
|
21
35
|
// `await` zawsze coś dostał.
|
|
22
36
|
onOpenChange: (open) => {
|
|
23
37
|
if (!open)
|
|
24
38
|
rejectKptConfirm();
|
|
25
|
-
}, header: request?.header ?? t('confirm.header'), locale: locale, dictionary: dictionary, children: [_jsxs("div", { className: "kpt-confirm__body", children: [request?.icon ? (_jsx(KptIcon, { className: "kpt-confirm__icon", name: request.icon, "data-variant": request.variant ?? 'filled' })) : null, _jsx("p", { className: "kpt-confirm__message", children: request?.message })] }), _jsxs(KptDialog.Footer, { children: [_jsx(KptButton, { variant: "text", onClick: rejectKptConfirm, children: request?.rejectLabel ?? t('confirm.reject') }), _jsx(KptButton, { variant: request?.variant ?? 'filled', onClick: acceptKptConfirm, children: request?.acceptLabel ?? t('confirm.accept') })] })] }) }));
|
|
39
|
+
}, header: request?.header ?? t('confirm.header'), role: "alertdialog", "aria-describedby": messageId, locale: locale, dictionary: dictionary, children: [_jsxs("div", { className: "kpt-confirm__body", children: [request?.icon ? (_jsx(KptIcon, { className: "kpt-confirm__icon", name: request.icon, "data-variant": request.variant ?? 'filled' })) : null, _jsx("p", { className: "kpt-confirm__message", id: messageId, children: request?.message })] }), _jsxs(KptDialog.Footer, { children: [_jsx(KptButton, { ref: rejectRef, variant: "text", onClick: rejectKptConfirm, children: request?.rejectLabel ?? t('confirm.reject') }), _jsx(KptButton, { variant: request?.variant ?? 'filled', onClick: acceptKptConfirm, children: request?.acceptLabel ?? t('confirm.accept') })] })] }) }));
|
|
26
40
|
}
|
package/dist/confirm/llms.txt
CHANGED
|
@@ -40,5 +40,7 @@ Inherited from `KptDialog`; the icon follows the variant (`--kpt-color-danger` /
|
|
|
40
40
|
`--kpt-color-primary`).
|
|
41
41
|
|
|
42
42
|
## Accessibility
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
Built on `KptDialog`, so the focus trap, Escape and the backdrop are the native `<dialog>`'s.
|
|
44
|
+
The window is `role="alertdialog"` named by its header, with the message tied to it through
|
|
45
|
+
`aria-describedby`, so a screen reader reads the question as soon as it opens. Focus starts on the
|
|
46
|
+
reject button — the safe answer, so a stray `Enter` deletes nothing. Escape and × reject.
|
|
@@ -9,6 +9,11 @@ export interface KptContextMenuProps<T> extends Omit<ComponentPropsWithRef<'div'
|
|
|
9
9
|
/**
|
|
10
10
|
* Menu kontekstowe (PPM) na obszarze dzieci. Panel pozycjonowany do kursora.
|
|
11
11
|
*
|
|
12
|
+
* Klawiatura jak w `KptMenu` (wspólne `menu-keys.ts`): po otwarciu fokus na pierwszej pozycji,
|
|
13
|
+
* strzałki / `Home` / `End` chodzą po pozycjach, `Escape` i `Tab` zamykają, a fokus wraca tam,
|
|
14
|
+
* gdzie był. `Shift+F10` i klawisz Menu na skupionym elemencie obszaru też otwierają menu
|
|
15
|
+
* (panel staje wtedy przy tym elemencie). Panel jest dociskany do krawędzi okna.
|
|
16
|
+
*
|
|
12
17
|
* W odróżnieniu od `KptMenu` panel nie idzie przez `KptOverlay` — nie ma tu wyzwalacza,
|
|
13
18
|
* względem którego dałoby się go przypiąć; pozycję wyznaczają współrzędne kliknięcia,
|
|
14
19
|
* a wygląd i warstwę niesie wspólny SCSS (`.kpt-context-menu__panel`).
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cn } from "../internal/cn.js";
|
|
4
4
|
import { MenuRows } from "../menu/menu-rows.js";
|
|
5
|
+
import { focusMenuEdge, handleMenuKeydown } from "../menu/menu-keys.js";
|
|
6
|
+
/** Odstęp panelu od krawędzi okna przy dociskaniu. */
|
|
7
|
+
const EDGE = 8;
|
|
5
8
|
/**
|
|
6
9
|
* Menu kontekstowe (PPM) na obszarze dzieci. Panel pozycjonowany do kursora.
|
|
7
10
|
*
|
|
11
|
+
* Klawiatura jak w `KptMenu` (wspólne `menu-keys.ts`): po otwarciu fokus na pierwszej pozycji,
|
|
12
|
+
* strzałki / `Home` / `End` chodzą po pozycjach, `Escape` i `Tab` zamykają, a fokus wraca tam,
|
|
13
|
+
* gdzie był. `Shift+F10` i klawisz Menu na skupionym elemencie obszaru też otwierają menu
|
|
14
|
+
* (panel staje wtedy przy tym elemencie). Panel jest dociskany do krawędzi okna.
|
|
15
|
+
*
|
|
8
16
|
* W odróżnieniu od `KptMenu` panel nie idzie przez `KptOverlay` — nie ma tu wyzwalacza,
|
|
9
17
|
* względem którego dałoby się go przypiąć; pozycję wyznaczają współrzędne kliknięcia,
|
|
10
18
|
* a wygląd i warstwę niesie wspólny SCSS (`.kpt-context-menu__panel`).
|
|
@@ -17,12 +25,48 @@ import { MenuRows } from "../menu/menu-rows.js";
|
|
|
17
25
|
export function KptContextMenu({ items = [], children, onSelect, className, ...rest }) {
|
|
18
26
|
const [open, setOpen] = useState(false);
|
|
19
27
|
const [point, setPoint] = useState({ x: 0, y: 0 });
|
|
28
|
+
const panelRef = useRef(null);
|
|
29
|
+
/** Element, który miał fokus przed otwarciem — tam fokus wraca po zamknięciu. */
|
|
30
|
+
const returnTo = useRef(null);
|
|
20
31
|
const onContextMenu = (event) => {
|
|
21
32
|
event.preventDefault();
|
|
22
|
-
|
|
33
|
+
const active = document.activeElement;
|
|
34
|
+
returnTo.current = active instanceof HTMLElement ? active : null;
|
|
35
|
+
// Z klawiatury (`Shift+F10`, klawisz Menu) zdarzenie nie niesie pozycji myszy.
|
|
36
|
+
if (event.clientX === 0 && event.clientY === 0 && event.target instanceof HTMLElement) {
|
|
37
|
+
const r = event.target.getBoundingClientRect();
|
|
38
|
+
setPoint({ x: r.left, y: r.bottom });
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
setPoint({ x: event.clientX, y: event.clientY });
|
|
42
|
+
}
|
|
23
43
|
setOpen(true);
|
|
24
44
|
};
|
|
25
|
-
|
|
45
|
+
// Po otwarciu: dociśnięcie do okna i fokus na pierwszej pozycji.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const panel = panelRef.current;
|
|
48
|
+
if (!open || !panel)
|
|
49
|
+
return;
|
|
50
|
+
const r = panel.getBoundingClientRect();
|
|
51
|
+
const maxX = window.innerWidth - r.width - EDGE;
|
|
52
|
+
const maxY = window.innerHeight - r.height - EDGE;
|
|
53
|
+
if (point.x > maxX || point.y > maxY) {
|
|
54
|
+
setPoint({ x: Math.max(EDGE, Math.min(point.x, maxX)), y: Math.max(EDGE, Math.min(point.y, maxY)) });
|
|
55
|
+
}
|
|
56
|
+
focusMenuEdge(panel, 'first');
|
|
57
|
+
// Celowo tylko `open`: dociśnięcie zmienia `point`, a ponowne uruchomienie przeniosłoby fokus.
|
|
58
|
+
}, [open]);
|
|
59
|
+
const close = () => {
|
|
60
|
+
setOpen(false);
|
|
61
|
+
const target = returnTo.current;
|
|
62
|
+
returnTo.current = null;
|
|
63
|
+
if (target?.isConnected)
|
|
64
|
+
target.focus();
|
|
65
|
+
};
|
|
66
|
+
const onPanelKeyDown = (event) => {
|
|
67
|
+
if (panelRef.current && handleMenuKeydown(panelRef.current, event.nativeEvent) === 'close')
|
|
68
|
+
close();
|
|
69
|
+
};
|
|
26
70
|
const pick = (item) => {
|
|
27
71
|
if (item.disabled)
|
|
28
72
|
return;
|
|
@@ -32,5 +76,5 @@ export function KptContextMenu({ items = [], children, onSelect, className, ...r
|
|
|
32
76
|
return (_jsxs("div", { className: cn('kpt-context-menu-host', className), ...rest, children: [_jsx("div", { className: "kpt-context-menu__area", onContextMenu: onContextMenu, children: children }), open ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "kpt-context-menu__backdrop", onClick: close, onContextMenu: (event) => {
|
|
33
77
|
event.preventDefault();
|
|
34
78
|
close();
|
|
35
|
-
} }), _jsx("div", { className: "kpt-context-menu__panel", role: "menu", style: { left: point.x, top: point.y }, children: _jsx(MenuRows, { items: items, block: "kpt-context-menu", onPick: pick }) })] })) : null] }));
|
|
79
|
+
} }), _jsx("div", { ref: panelRef, className: "kpt-context-menu__panel", role: "menu", style: { left: point.x, top: point.y }, onKeyDown: onPanelKeyDown, children: _jsx(MenuRows, { items: items, block: "kpt-context-menu", onPick: pick }) })] })) : null] }));
|
|
36
80
|
}
|
|
@@ -23,7 +23,15 @@ the position comes from the click coordinates, and the look and the layer come f
|
|
|
23
23
|
|
|
24
24
|
## Closing
|
|
25
25
|
A left click on the backdrop closes it; a right click on the backdrop closes it too and does not
|
|
26
|
-
open the browser's own menu. Picking an item closes it and reports the choice.
|
|
26
|
+
open the browser's own menu. Picking an item closes it and reports the choice; `Escape` closes it too.
|
|
27
|
+
|
|
28
|
+
## Keyboard
|
|
29
|
+
The same model as `KptMenu` (the shared `menu-keys.ts`): opening moves focus to the first item, arrows /
|
|
30
|
+
`Home` / `End` move between items (disabled ones and separators are skipped), `Escape` and `Tab` close
|
|
31
|
+
it and focus returns to where it was. `Shift+F10` and the context-menu key on a focused element inside
|
|
32
|
+
the area open it too — the browser then sends `contextmenu` without mouse coordinates, so the panel is
|
|
33
|
+
placed at that element. The panel is kept inside the viewport (8px from the edges). Touch screens have
|
|
34
|
+
no right click, so still offer the same actions through a visible button with `KptMenu`.
|
|
27
35
|
|
|
28
36
|
## Examples
|
|
29
37
|
<KptContextMenu items={items} onSelect={onSelect}>
|
|
@@ -34,6 +42,5 @@ open the browser's own menu. Picking an item closes it and reports the choice.
|
|
|
34
42
|
`--kpt-color-surface-raised`, `--kpt-color-border`, `--kpt-elevation-3`, `--kpt-color-danger`.
|
|
35
43
|
|
|
36
44
|
## Accessibility
|
|
37
|
-
The panel is `role="menu"` with `role="menuitem"` buttons
|
|
38
|
-
|
|
39
|
-
with a `KptMenu` when they must be reachable from the keyboard.
|
|
45
|
+
The panel is `role="menu"` with `role="menuitem"` buttons at `tabindex="-1"` (one Tab stop). Keyboard — see
|
|
46
|
+
"Keyboard" above.
|
|
@@ -13,6 +13,14 @@ export interface KptDateRangeProps extends Omit<ComponentPropsWithRef<'div'>, 'o
|
|
|
13
13
|
minuteStep?: number;
|
|
14
14
|
placeholder?: string;
|
|
15
15
|
displayFormat?: 'pl' | 'iso';
|
|
16
|
+
/** Najwcześniejsza dozwolona data 'RRRR-MM-DD' (włącznie). */
|
|
17
|
+
minDate?: string | null;
|
|
18
|
+
/** Najpóźniejsza dozwolona data 'RRRR-MM-DD' (włącznie). */
|
|
19
|
+
maxDate?: string | null;
|
|
20
|
+
/** Nazwa dostępna pola, gdy nie opisuje go `<label for>`. */
|
|
21
|
+
ariaLabel?: string | null;
|
|
22
|
+
/** Id pola (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
|
|
23
|
+
id?: string;
|
|
16
24
|
disabled?: boolean;
|
|
17
25
|
invalid?: boolean;
|
|
18
26
|
touched?: boolean;
|
|
@@ -23,8 +31,13 @@ export interface KptDateRangeProps extends Omit<ComponentPropsWithRef<'div'>, 'o
|
|
|
23
31
|
* `visibleMonths` pokazuje kilka miesięcy obok siebie, a zakres podświetla się ciągłym pasmem
|
|
24
32
|
* przez wszystkie panele.
|
|
25
33
|
*
|
|
34
|
+
* Klawiatura jak w `KptDatepicker`: `Enter`/`Spacja`/`↓` otwierają panel z fokusem na początku
|
|
35
|
+
* zakresu (albo dziś), strzałki, `Home`/`End` i `PageUp`/`PageDown` (z `Shift` — rok) chodzą po
|
|
36
|
+
* dniach przez wszystkie panele, `Tab` krąży w panelu, `Escape` i wybór końca oddają fokus polu.
|
|
37
|
+
* Krańce i środek zakresu są w nazwie dnia („…, początek zakresu”), a wybór ogłasza region `polite`.
|
|
38
|
+
*
|
|
26
39
|
* @example
|
|
27
40
|
* <KptDateRange value={period} onValueChange={setPeriod} visibleMonths={2} />
|
|
28
41
|
* <KptDateRange mode="datetime" hourFormat={12} value={span} onValueChange={setSpan} />
|
|
29
42
|
*/
|
|
30
|
-
export declare function KptDateRange({ value, defaultValue, onValueChange, mode, visibleMonths, hourFormat, minuteStep, placeholder, displayFormat, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptDateRangeProps>): import("react").JSX.Element;
|
|
43
|
+
export declare function KptDateRange({ value, defaultValue, onValueChange, mode, visibleMonths, hourFormat, minuteStep, placeholder, displayFormat, minDate, maxDate, ariaLabel, id, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptDateRangeProps>): import("react").JSX.Element;
|
|
@@ -1,31 +1,48 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState } from 'react';
|
|
3
|
-
import { buildRangeGrid, clampVisibleMonths, datePart, formatDate, formatTime, pad, parseTime, pickRangeDay, timePart, toIso, } from '@konce-pt/datetime';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { buildRangeGrid, clampVisibleMonths, datePart, formatDate, formatTime, initialFocusDate, moveFocusDate, pad, parseDate, parseTime, pickRangeDay, timePart, toIso, } from '@konce-pt/datetime';
|
|
4
4
|
import { monthNames, weekdayNames } from '@konce-pt/i18n';
|
|
5
5
|
import { cn } from "../internal/cn.js";
|
|
6
6
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
7
7
|
import { KptOverlay } from "../internal/overlay.js";
|
|
8
8
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
9
|
-
import {
|
|
9
|
+
import { useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
10
10
|
import { KptButton } from "../button/button.js";
|
|
11
11
|
import { KptIcon } from "../icon/icon.js";
|
|
12
12
|
import { KptClock } from "../clock/clock.js";
|
|
13
|
+
import { useKptId } from "../internal/use-id.js";
|
|
14
|
+
import { focusCalendarActive, trapTab } from "../internal/calendar-focus.js";
|
|
13
15
|
const EMPTY_RANGE = { start: '', end: '' };
|
|
14
16
|
/**
|
|
15
17
|
* Wybór zakresu dat (i godzin) z kalendarzem. Pierwszy klik ustawia początek, drugi koniec;
|
|
16
18
|
* `visibleMonths` pokazuje kilka miesięcy obok siebie, a zakres podświetla się ciągłym pasmem
|
|
17
19
|
* przez wszystkie panele.
|
|
18
20
|
*
|
|
21
|
+
* Klawiatura jak w `KptDatepicker`: `Enter`/`Spacja`/`↓` otwierają panel z fokusem na początku
|
|
22
|
+
* zakresu (albo dziś), strzałki, `Home`/`End` i `PageUp`/`PageDown` (z `Shift` — rok) chodzą po
|
|
23
|
+
* dniach przez wszystkie panele, `Tab` krąży w panelu, `Escape` i wybór końca oddają fokus polu.
|
|
24
|
+
* Krańce i środek zakresu są w nazwie dnia („…, początek zakresu”), a wybór ogłasza region `polite`.
|
|
25
|
+
*
|
|
19
26
|
* @example
|
|
20
27
|
* <KptDateRange value={period} onValueChange={setPeriod} visibleMonths={2} />
|
|
21
28
|
* <KptDateRange mode="datetime" hourFormat={12} value={span} onValueChange={setSpan} />
|
|
22
29
|
*/
|
|
23
|
-
export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange, mode = 'date', visibleMonths = 1, hourFormat = 24, minuteStep = 1, placeholder, displayFormat = 'pl', disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
30
|
+
export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange, mode = 'date', visibleMonths = 1, hourFormat = 24, minuteStep = 1, placeholder, displayFormat = 'pl', minDate = null, maxDate = null, ariaLabel = null, id, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
|
|
24
31
|
const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
|
|
25
|
-
|
|
32
|
+
/** `div` z rolą nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
|
|
33
|
+
const field = useKptFieldA11y(id, 'kpt-date-range');
|
|
26
34
|
const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
|
|
27
35
|
const [isOpen, setOpen] = useState(false);
|
|
28
36
|
const [anchor, setAnchor] = useState(null);
|
|
37
|
+
const triggerRef = useRef(null);
|
|
38
|
+
const panelRef = useRef(null);
|
|
39
|
+
const panelId = useKptId(undefined, 'kpt-date-range-panel');
|
|
40
|
+
/** Dzień z fokusem — jedyny przystanek Tab siatek dni (roving tabindex przez wszystkie panele). */
|
|
41
|
+
const [focusIso, setFocusIso] = useState('');
|
|
42
|
+
/** Komunikat regionu `polite` po wyborze początku i całego zakresu. */
|
|
43
|
+
const [announcement, setAnnouncement] = useState('');
|
|
44
|
+
/** Prośba o przeniesienie fokusu po najbliższym renderze (otwarcie, ruch klawiszem). */
|
|
45
|
+
const wantFocus = useRef(false);
|
|
29
46
|
const today = useRef(new Date()).current;
|
|
30
47
|
const todayIso = toIso(today);
|
|
31
48
|
const showInvalid = invalid && (touched ?? true);
|
|
@@ -40,6 +57,22 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
|
|
|
40
57
|
const weekdays = weekdayNames(activeLocale);
|
|
41
58
|
const panelCount = mode === 'time' ? 1 : clampVisibleMonths(visibleMonths);
|
|
42
59
|
const multi = panelCount > 1;
|
|
60
|
+
const dayFormat = new Intl.DateTimeFormat(activeLocale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
|
61
|
+
const dayName = (iso) => {
|
|
62
|
+
const date = parseDate(iso);
|
|
63
|
+
return date ? dayFormat.format(date) : iso;
|
|
64
|
+
};
|
|
65
|
+
/** Pełna nazwa dnia z rolą w zakresie — kolor pasma nie jest jedynym nośnikiem informacji. */
|
|
66
|
+
const dayLabel = (cell) => {
|
|
67
|
+
const name = dayName(cell.iso);
|
|
68
|
+
if (cell.isStart)
|
|
69
|
+
return t('dateRange.rangeStart', { date: name });
|
|
70
|
+
if (cell.isEnd)
|
|
71
|
+
return t('dateRange.rangeEnd', { date: name });
|
|
72
|
+
if (cell.inRange)
|
|
73
|
+
return t('dateRange.inRange', { date: name });
|
|
74
|
+
return name;
|
|
75
|
+
};
|
|
43
76
|
const fmtTime = (hhmm) => {
|
|
44
77
|
const parsed = parseTime(hhmm);
|
|
45
78
|
return parsed ? formatTime(parsed.h, parsed.m, hourFormat) : '';
|
|
@@ -68,9 +101,11 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
|
|
|
68
101
|
return;
|
|
69
102
|
if (!isOpen) {
|
|
70
103
|
const { start, end } = current;
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
const focus = initialFocusDate(datePart(start), todayIso, minDate, maxDate);
|
|
105
|
+
setFocusIso(focus);
|
|
106
|
+
setAnnouncement('');
|
|
107
|
+
setPage({ y: +focus.slice(0, 4), m: +focus.slice(5, 7) - 1 });
|
|
108
|
+
wantFocus.current = true;
|
|
74
109
|
if (mode === 'datetime') {
|
|
75
110
|
setStartDate(datePart(start));
|
|
76
111
|
setEndDate(datePart(end));
|
|
@@ -91,6 +126,59 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
|
|
|
91
126
|
setOpen(false);
|
|
92
127
|
onTouch?.();
|
|
93
128
|
};
|
|
129
|
+
/** Zamknięcie z powrotem fokusu na pole — po wyborze z klawiatury, `Escape`. */
|
|
130
|
+
const closeAndFocus = () => {
|
|
131
|
+
close();
|
|
132
|
+
triggerRef.current?.focus();
|
|
133
|
+
};
|
|
134
|
+
// Fokus po renderze. Panel w nakładce jest niewidoczny do pierwszego pomiaru — stąd rAF.
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!isOpen || !wantFocus.current)
|
|
137
|
+
return;
|
|
138
|
+
const frame = requestAnimationFrame(() => {
|
|
139
|
+
wantFocus.current = false;
|
|
140
|
+
focusCalendarActive(panelRef.current, focusIso);
|
|
141
|
+
});
|
|
142
|
+
return () => cancelAnimationFrame(frame);
|
|
143
|
+
});
|
|
144
|
+
/** Przesuwa okno paneli tak, żeby miesiąc dnia `iso` był widoczny — z najmniejszym skokiem. */
|
|
145
|
+
const revealMonth = (iso) => {
|
|
146
|
+
const target = +iso.slice(0, 4) * 12 + (+iso.slice(5, 7) - 1);
|
|
147
|
+
const first = page.y * 12 + page.m;
|
|
148
|
+
if (target < first)
|
|
149
|
+
setPage({ y: Math.floor(target / 12), m: target % 12 });
|
|
150
|
+
else if (target > first + panelCount - 1) {
|
|
151
|
+
const start = target - panelCount + 1;
|
|
152
|
+
setPage({ y: Math.floor(start / 12), m: start % 12 });
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const onDayKeyDown = (event) => {
|
|
156
|
+
const rtl = !!panelRef.current && getComputedStyle(panelRef.current).direction === 'rtl';
|
|
157
|
+
const next = moveFocusDate(focusIso, event.key, { shift: event.shiftKey, rtl, minDate, maxDate });
|
|
158
|
+
if (next === null)
|
|
159
|
+
return;
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
setFocusIso(next);
|
|
162
|
+
revealMonth(next);
|
|
163
|
+
wantFocus.current = true;
|
|
164
|
+
};
|
|
165
|
+
/** Strzałki ‹ ›: fokus zostaje na strzałce, ale przystanek siatki idzie za stroną. */
|
|
166
|
+
const turnPage = (dir) => {
|
|
167
|
+
const first = page.y * 12 + page.m + dir;
|
|
168
|
+
setPage({ y: Math.floor(first / 12), m: ((first % 12) + 12) % 12 });
|
|
169
|
+
const moved = moveFocusDate(focusIso, dir === 1 ? 'PageDown' : 'PageUp', { minDate, maxDate });
|
|
170
|
+
if (moved) {
|
|
171
|
+
const target = +moved.slice(0, 4) * 12 + (+moved.slice(5, 7) - 1);
|
|
172
|
+
// Nowe okno zaczyna się od `first` — dzień spoza niego nie dostałby przystanku Tab.
|
|
173
|
+
if (target >= first && target < first + panelCount)
|
|
174
|
+
setFocusIso(moved);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const announce = (start, end) => {
|
|
178
|
+
setAnnouncement(end
|
|
179
|
+
? t('dateRange.picked', { start: dayName(start), end: dayName(end) })
|
|
180
|
+
: t('dateRange.pickEnd', { date: dayName(start) }));
|
|
181
|
+
};
|
|
94
182
|
const panels = Array.from({ length: panelCount }, (_, i) => {
|
|
95
183
|
const first = new Date(page.y, page.m + i, 1);
|
|
96
184
|
const py = first.getFullYear();
|
|
@@ -104,21 +192,26 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
|
|
|
104
192
|
end: mode === 'datetime' ? endDate : current.end,
|
|
105
193
|
todayIso,
|
|
106
194
|
hideOutside: multi,
|
|
195
|
+
minDate,
|
|
196
|
+
maxDate,
|
|
107
197
|
}),
|
|
108
198
|
};
|
|
109
199
|
});
|
|
110
200
|
const selectDay = (iso) => {
|
|
201
|
+
setFocusIso(iso);
|
|
111
202
|
if (mode === 'datetime') {
|
|
112
203
|
const next = pickRangeDay({ start: startDate, end: endDate }, iso);
|
|
113
204
|
setStartDate(next.start);
|
|
114
205
|
setEndDate(next.end);
|
|
206
|
+
announce(next.start, next.end);
|
|
115
207
|
return; // czas do wyboru → zamknięcie przez „Gotowe"
|
|
116
208
|
}
|
|
117
209
|
const next = pickRangeDay(current, iso);
|
|
118
210
|
setCurrent(next);
|
|
211
|
+
announce(next.start, next.end);
|
|
119
212
|
// Panel zostaje otwarty do czasu domknięcia zakresu — inaczej drugi klik nie miałby gdzie paść.
|
|
120
213
|
if (next.end)
|
|
121
|
-
|
|
214
|
+
closeAndFocus();
|
|
122
215
|
};
|
|
123
216
|
const commit = () => {
|
|
124
217
|
if (mode === 'time') {
|
|
@@ -135,7 +228,7 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
|
|
|
135
228
|
const end = `${endDate || startDate}T${endTime || '00:00'}`;
|
|
136
229
|
setCurrent(start <= end ? { start, end } : { start: end, end: start });
|
|
137
230
|
}
|
|
138
|
-
|
|
231
|
+
closeAndFocus();
|
|
139
232
|
};
|
|
140
233
|
const clear = (event) => {
|
|
141
234
|
event.stopPropagation();
|
|
@@ -145,11 +238,34 @@ export function KptDateRange({ value, defaultValue = EMPTY_RANGE, onValueChange,
|
|
|
145
238
|
};
|
|
146
239
|
const clockValue = timeEnd === 'start' ? startTime : endTime;
|
|
147
240
|
const setClockValue = timeEnd === 'start' ? setStartTime : setEndTime;
|
|
148
|
-
return (_jsxs("div", { className: cn('kpt-date-range-host', className), ...rest, children: [_jsxs("div", { ref:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
241
|
+
return (_jsxs("div", { className: cn('kpt-date-range-host', className), ...rest, children: [_jsxs("div", { ref: (el) => {
|
|
242
|
+
triggerRef.current = el;
|
|
243
|
+
setAnchor(el);
|
|
244
|
+
}, className: cn('kpt-date-range', isOpen && 'kpt-date-range--open', disabled && 'kpt-date-range--disabled', showInvalid && 'kpt-date-range--invalid'), role: "button", tabIndex: 0, "aria-haspopup": "dialog", "aria-expanded": isOpen, "aria-controls": isOpen ? panelId : undefined, id: field.id, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, "aria-disabled": disabled || undefined, "aria-invalid": showInvalid || undefined, "aria-label": ariaLabel ?? undefined, onClick: toggleOpen, onKeyDown: (event) => {
|
|
245
|
+
if (disabled)
|
|
246
|
+
return;
|
|
247
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
if (!isOpen)
|
|
250
|
+
toggleOpen();
|
|
251
|
+
}
|
|
252
|
+
else if ((event.key === 'Delete' || event.key === 'Backspace') && current.start) {
|
|
253
|
+
event.preventDefault();
|
|
254
|
+
clear(event);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
// Przy otwartym panelu fokus idzie do kalendarza — „dotknięcie” wyśle dopiero zamknięcie.
|
|
258
|
+
onBlur: () => !isOpen && onTouch?.(), children: [display ? (_jsx("span", { className: "kpt-date-range__value", children: display })) : (_jsx("span", { className: "kpt-date-range__placeholder", children: placeholder ?? t('dateRange.placeholder') })), _jsx("span", { className: "kpt-date-range__spacer" }), display ? (_jsx("button", { type: "button", className: "kpt-date-range__clear", tabIndex: -1, onClick: clear, "aria-label": t('common.clear'), children: _jsx(KptIcon, { name: "x" }) })) : null, _jsx(KptIcon, { className: "kpt-date-range__icon", name: "calendar" })] }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, offset: 4, children: _jsxs("div", { ref: panelRef, id: panelId, role: "dialog", "aria-label": t('dateRange.dialog'), className: cn('kpt-calendar', mode === 'time' && 'kpt-calendar--clock', mode !== 'time' && 'kpt-calendar--range', multi && mode !== 'time' && 'kpt-calendar--multi'), onKeyDown: (event) => {
|
|
259
|
+
if (event.key === 'Escape') {
|
|
260
|
+
event.preventDefault();
|
|
261
|
+
event.stopPropagation();
|
|
262
|
+
closeAndFocus();
|
|
263
|
+
}
|
|
264
|
+
else if (event.key === 'Tab') {
|
|
265
|
+
trapTab(event.currentTarget, event.nativeEvent);
|
|
266
|
+
}
|
|
267
|
+
}, children: [_jsx("span", { className: "kpt-calendar__live", "aria-live": "polite", children: announcement }), mode === 'time' ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kpt-range-tabs", children: [_jsx("button", { type: "button", className: cn(timeEnd === 'start' && 'is-active'), onClick: () => setTimeEnd('start'), children: t('dateRange.from') }), _jsx("button", { type: "button", className: cn(timeEnd === 'end' && 'is-active'), onClick: () => setTimeEnd('end'), children: t('dateRange.to') })] }), _jsx(KptClock
|
|
268
|
+
// Klucz na edytowanym końcu wymusza remount przy przełączeniu Od/Koniec — tarcza ma
|
|
269
|
+
// wtedy wrócić do etapu godzin, tak jak dwie osobne instancje w porcie Angulara.
|
|
270
|
+
, { value: clockValue, onValueChange: setClockValue, hourFormat: hourFormat, minuteStep: minuteStep }, timeEnd), _jsx("div", { className: "kpt-calendar__footer", children: _jsx(KptButton, { size: "sm", onClick: commit, children: t('common.done') }) })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "kpt-calendar__panels", children: panels.map((panel, index) => (_jsxs("div", { className: "kpt-calendar__panel", children: [_jsxs("div", { className: "kpt-calendar__header", children: [index === 0 ? (_jsx("button", { type: "button", className: "kpt-calendar__nav", onClick: () => turnPage(-1), "aria-label": t('datepicker.prevMonth'), children: _jsx(KptIcon, { name: "chevron-left" }) })) : (_jsx("span", { className: "kpt-calendar__nav-slot" })), _jsx("span", { className: "kpt-calendar__title", children: panel.label }), index === panels.length - 1 ? (_jsx("button", { type: "button", className: "kpt-calendar__nav", onClick: () => turnPage(1), "aria-label": t('datepicker.nextMonth'), children: _jsx(KptIcon, { name: "chevron-right" }) })) : (_jsx("span", { className: "kpt-calendar__nav-slot" }))] }), _jsx("div", { className: "kpt-calendar__weekdays", children: weekdays.map((weekday) => (_jsx("span", { className: "kpt-calendar__weekday", children: weekday }, weekday))) }), _jsx("div", { className: "kpt-calendar__grid", onKeyDown: onDayKeyDown, children: panel.weeks.flat().map((cell) => cell.blank ? (_jsx("span", { className: "kpt-calendar__day kpt-calendar__day--blank" }, cell.iso)) : (_jsx("button", { type: "button", className: cn('kpt-calendar__day', !cell.inMonth && 'kpt-calendar__day--muted', cell.today && 'kpt-calendar__day--today', cell.inRange && 'kpt-calendar__day--range', cell.rangeStart && 'kpt-calendar__day--range-start', cell.rangeEnd && 'kpt-calendar__day--range-end', (cell.isStart || cell.isEnd) && 'kpt-calendar__day--selected'), "data-iso": cell.iso, tabIndex: cell.iso === focusIso ? 0 : -1, "aria-label": dayLabel(cell), "aria-current": cell.today ? 'date' : undefined, "aria-pressed": cell.isStart || cell.isEnd, disabled: cell.disabled, onClick: () => selectDay(cell.iso), children: cell.day }, cell.iso))) })] }, panel.key))) }), mode === 'datetime' ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kpt-range-tabs", children: [_jsx("button", { type: "button", className: cn(timeEnd === 'start' && 'is-active'), onClick: () => setTimeEnd('start'), children: t('dateRange.fromTime') }), _jsx("button", { type: "button", className: cn(timeEnd === 'end' && 'is-active'), onClick: () => setTimeEnd('end'), children: t('dateRange.toTime') })] }), _jsx(KptClock, { value: clockValue, onValueChange: setClockValue, hourFormat: hourFormat, minuteStep: minuteStep }, timeEnd), _jsx("div", { className: "kpt-calendar__footer", children: _jsx(KptButton, { size: "sm", onClick: commit, children: t('common.done') }) })] })) : null] }))] }) })] }));
|
|
155
271
|
}
|
package/dist/date-range/llms.txt
CHANGED
|
@@ -20,7 +20,10 @@ Import: `import { KptDateRange, type KptDateRangeValue } from '@konce-pt/react';
|
|
|
20
20
|
- `hourFormat`: 12 | 24 (default 24); `minuteStep`: number (default 1)
|
|
21
21
|
- `placeholder` — without one it falls back to `dateRange.placeholder`
|
|
22
22
|
- `displayFormat`: `'pl'` (default) | `'iso'`
|
|
23
|
-
- `
|
|
23
|
+
- `minDate`, `maxDate`: 'YYYY-MM-DD' bounds (inclusive); days outside are disabled
|
|
24
|
+
- `ariaLabel` — the accessible name when no `<label for>` describes the field
|
|
25
|
+
- `disabled`, `invalid`, `touched`
|
|
26
|
+
- `onTouch()` — fires when the panel closes or focus leaves the closed trigger
|
|
24
27
|
- `className`, `ref` and every other `<div>` prop go to the host
|
|
25
28
|
|
|
26
29
|
## Two clicks, one band
|
|
@@ -31,6 +34,18 @@ The panel stays open until the range closes — the second click needs somewhere
|
|
|
31
34
|
With several panels the adjacent-month days become blank slots: otherwise the same date would be
|
|
32
35
|
clickable twice, and the band would paint in both places.
|
|
33
36
|
|
|
37
|
+
## Keyboard
|
|
38
|
+
As in the date picker: the trigger has `aria-haspopup="dialog"` and `aria-expanded`; `Enter`, `Space`
|
|
39
|
+
and `↓` open the panel (a `role="dialog"` named "Choose a date range") with focus on the range start,
|
|
40
|
+
or today. The arrows, `Home`/`End` and `PageUp`/`PageDown` (with `Shift` — a year) walk the days across
|
|
41
|
+
every panel, shifting the set of months when focus leaves it. `Tab` cycles inside the panel, `Escape`
|
|
42
|
+
closes it, and picking the end (or "Done") returns focus to the trigger. `Delete`/`Backspace` on the
|
|
43
|
+
trigger clears the range.
|
|
44
|
+
|
|
45
|
+
Each day is named with the full date and its role in the range ("…, start of range", "…, end of range",
|
|
46
|
+
"…, in range") — the band colour is not the only carrier. The ends have `aria-pressed`, today has
|
|
47
|
+
`aria-current="date"`. After the first pick a polite live region says "Start: … Choose the end date.".
|
|
48
|
+
|
|
34
49
|
## The clock remounts when the end switches
|
|
35
50
|
`key={timeEnd}` on `KptClock`, so the dial returns to the hours stage after the From/To toggle —
|
|
36
51
|
in Angular that came for free from two separate template branches.
|
|
@@ -45,10 +60,18 @@ in Angular that came for free from two separate template branches.
|
|
|
45
60
|
Day and month names come from `Intl`; "From/To", the navigation, "Done" and the placeholder come
|
|
46
61
|
from the `dateRange`, `datepicker` and `common` namespaces.
|
|
47
62
|
|
|
63
|
+
## Label and description
|
|
64
|
+
`id` goes to the focusable element, not the host; without one it comes from `KptFormField` or is
|
|
65
|
+
generated. A `div` with a role is not named by `label[for]`, so inside `KptFormField` it gets
|
|
66
|
+
`aria-labelledby` pointing at the wrapper's label (unless `ariaLabel` is set), and the hint or error
|
|
67
|
+
is added to `aria-describedby`. New: `id`; the trigger is the element that carries it.
|
|
68
|
+
|
|
48
69
|
## Tokens
|
|
49
70
|
The field `--kpt-form-field-*`; the calendar `--kpt-color-surface-raised`, the range
|
|
50
71
|
`--kpt-color-primary`.
|
|
51
72
|
|
|
52
73
|
## Accessibility
|
|
53
|
-
The trigger is `role="button"` with
|
|
54
|
-
|
|
74
|
+
The trigger is `role="button"` with `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`;
|
|
75
|
+
the panel is a named `role="dialog"`; the days are real buttons named with the date and their range
|
|
76
|
+
role, and the time ends are switched with a two-button toggle rather than a custom control. The
|
|
77
|
+
keyboard is described above.
|