@astryxdesign/core 0.1.4-canary.39ffd21 → 0.1.4-canary.3d64ef2
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/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +8 -1
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +31 -11
- package/dist/Calendar/styles.d.ts +2 -2
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +2 -2
- package/dist/Card/Card.d.ts +4 -2
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +15 -7
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +1 -0
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +8 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -1
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +26 -3
- package/dist/CodeBlock/CodeBlock.d.ts +10 -1
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +23 -5
- package/dist/Collapsible/Collapsible.d.ts +9 -3
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +101 -13
- package/dist/Collapsible/CollapsibleGroup.d.ts +39 -7
- package/dist/Collapsible/CollapsibleGroup.d.ts.map +1 -1
- package/dist/Collapsible/CollapsibleGroup.js +70 -9
- package/dist/Collapsible/CollapsibleGroupContext.d.ts +31 -0
- package/dist/Collapsible/CollapsibleGroupContext.d.ts.map +1 -1
- package/dist/Collapsible/CollapsibleGroupContext.js +33 -3
- package/dist/Collapsible/index.d.ts +1 -0
- package/dist/Collapsible/index.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +2 -0
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +2 -0
- package/dist/Grid/Grid.d.ts +4 -3
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +26 -9
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +5 -1
- package/dist/InputGroup/InputGroup.js +2 -2
- package/dist/Item/Item.js +1 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +26 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +32 -16
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +87 -36
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +39 -21
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +2 -1
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +10 -2
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +6 -2
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +34 -7
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +14 -3
- package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
- package/dist/Table/plugins/columnResize/useTableColumnResize.js +5 -2
- package/dist/Table/plugins/pagination/paginateData.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/paginateData.js +5 -1
- package/dist/Table/plugins/selection/useTableSelectionState.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelectionState.js +11 -1
- package/dist/Table/plugins/sortable/useTableSortableState.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortableState.js +8 -4
- package/dist/Thumbnail/Thumbnail.d.ts +3 -2
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +1 -0
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +6 -1
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +11 -3
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +12 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +5 -2
- package/dist/astryx.css +7 -0
- package/dist/astryx.umd.js +48 -48
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/focusableSelector.d.ts +1 -1
- package/dist/hooks/focusableSelector.d.ts.map +1 -1
- package/dist/hooks/focusableSelector.js +1 -1
- package/dist/hooks/useFocusTrap.d.ts +3 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +38 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +10 -3
- package/dist/theme/hct.d.ts +0 -15
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +9 -9
- package/dist/theme/tokens.d.ts +10 -2
- package/dist/theme/tokens.d.ts.map +1 -1
- package/dist/theme/tokens.js +220 -4
- package/dist/utils/color.d.ts +42 -0
- package/dist/utils/color.d.ts.map +1 -0
- package/dist/utils/color.js +150 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +8 -5
- package/package.json +1 -1
- package/src/Avatar/Avatar.doc.mjs +11 -0
- package/src/Banner/Banner.doc.mjs +8 -0
- package/src/Banner/Banner.test.tsx +38 -0
- package/src/Banner/Banner.tsx +8 -1
- package/src/Calendar/Calendar.doc.mjs +1 -1
- package/src/Calendar/Calendar.test.tsx +112 -4
- package/src/Calendar/Calendar.tsx +26 -5
- package/src/Calendar/styles.ts +12 -2
- package/src/Card/Card.tsx +16 -6
- package/src/Carousel/Carousel.doc.mjs +12 -12
- package/src/Carousel/Carousel.test.tsx +13 -0
- package/src/Carousel/Carousel.tsx +1 -0
- package/src/Chat/Chat.doc.mjs +1 -1
- package/src/Chat/ChatToolCalls.test.tsx +55 -0
- package/src/Chat/ChatToolCalls.tsx +10 -12
- package/src/CheckboxInput/CheckboxInput.doc.mjs +3 -3
- package/src/CheckboxInput/CheckboxInput.test.tsx +71 -4
- package/src/CheckboxInput/CheckboxInput.tsx +2 -0
- package/src/Citation/Citation.doc.mjs +5 -0
- package/src/ClickableCard/ClickableCard.tsx +45 -2
- package/src/CodeBlock/CodeBlock.doc.mjs +7 -2
- package/src/CodeBlock/CodeBlock.test.tsx +87 -3
- package/src/CodeBlock/CodeBlock.tsx +43 -3
- package/src/Collapsible/Collapsible.doc.mjs +8 -4
- package/src/Collapsible/Collapsible.tsx +86 -10
- package/src/Collapsible/CollapsibleGroup.doc.mjs +27 -3
- package/src/Collapsible/CollapsibleGroup.test.tsx +288 -0
- package/src/Collapsible/CollapsibleGroup.tsx +107 -9
- package/src/Collapsible/CollapsibleGroupContext.tsx +42 -2
- package/src/Collapsible/index.ts +4 -0
- package/src/CommandPalette/CommandPaletteEmpty.doc.mjs +5 -0
- package/src/CommandPalette/CommandPaletteGroup.doc.mjs +10 -0
- package/src/CommandPalette/CommandPaletteInput.doc.mjs +6 -0
- package/src/CommandPalette/CommandPaletteItem.doc.mjs +6 -0
- package/src/CommandPalette/CommandPaletteList.doc.mjs +9 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +15 -0
- package/src/ContextMenu/ContextMenu.test.tsx +15 -0
- package/src/ContextMenu/ContextMenu.tsx +2 -0
- package/src/ContextMenu/ContextMenuItem.doc.mjs +6 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +70 -0
- package/src/DateTimeInput/DateTimeInput.tsx +2 -0
- package/src/Dialog/DialogHeader.doc.mjs +45 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -1
- package/src/Field/FieldLabel.test.tsx +2 -2
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
- package/src/Grid/Grid.test.tsx +43 -18
- package/src/Grid/Grid.tsx +26 -9
- package/src/Icon/Icon.test.tsx +41 -29
- package/src/Icon/Icon.tsx +6 -2
- package/src/InputGroup/InputGroup.tsx +2 -2
- package/src/Item/Item.tsx +1 -1
- package/src/Layout/LayoutContent.doc.mjs +12 -0
- package/src/Layout/LayoutPanel.doc.mjs +40 -0
- package/src/Lightbox/Lightbox.doc.mjs +5 -0
- package/src/Lightbox/Lightbox.test.tsx +123 -2
- package/src/Lightbox/Lightbox.tsx +29 -1
- package/src/NavMenu/NavMenu.doc.mjs +15 -0
- package/src/NumberInput/NumberInput.test.tsx +48 -3
- package/src/NumberInput/NumberInput.tsx +32 -15
- package/src/Overlay/Overlay.doc.mjs +6 -0
- package/src/Pagination/Pagination.test.tsx +143 -0
- package/src/Pagination/Pagination.tsx +75 -23
- package/src/Popover/Popover.test.tsx +65 -0
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/PowerSearch/PowerSearch.test.tsx +109 -1
- package/src/PowerSearch/PowerSearch.tsx +37 -16
- package/src/ProgressBar/ProgressBar.test.tsx +10 -0
- package/src/ProgressBar/ProgressBar.tsx +2 -1
- package/src/Resizable/ResizeHandle.test.tsx +180 -0
- package/src/Resizable/ResizeHandle.tsx +10 -2
- package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
- package/src/SelectableCard/SelectableCard.doc.mjs +1 -1
- package/src/SideNav/SideNav.doc.mjs +7 -1
- package/src/SideNav/SideNav.test.tsx +20 -0
- package/src/SideNav/SideNav.tsx +6 -2
- package/src/TabList/TabList.test.tsx +185 -1
- package/src/TabList/TabMenu.tsx +45 -9
- package/src/Table/BaseTable.tsx +13 -2
- package/src/Table/Table.test.tsx +51 -0
- package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +13 -0
- package/src/Table/plugins/columnResize/useTableColumnResize.tsx +3 -1
- package/src/Table/plugins/pagination/paginateData.ts +5 -1
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +13 -0
- package/src/Table/plugins/selection/useTableSelectionState.test.tsx +23 -0
- package/src/Table/plugins/selection/useTableSelectionState.tsx +11 -6
- package/src/Table/plugins/sortable/useTableSortableState.test.tsx +35 -1
- package/src/Table/plugins/sortable/useTableSortableState.tsx +8 -4
- package/src/TextArea/TextArea.test.tsx +3 -3
- package/src/TextInput/TextInput.test.tsx +3 -3
- package/src/Thumbnail/Thumbnail.test.tsx +36 -2
- package/src/Thumbnail/Thumbnail.tsx +4 -2
- package/src/TimeInput/TimeInput.test.tsx +37 -0
- package/src/TimeInput/TimeInput.tsx +11 -1
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Toast/Toast.tsx +11 -3
- package/src/Toast/ToastViewport.test.tsx +64 -0
- package/src/Toast/ToastViewport.tsx +12 -0
- package/src/ToggleButton/ToggleButton.doc.mjs +1 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
- package/src/TopNav/TopNav.doc.mjs +1 -1
- package/src/TreeList/TreeList.doc.mjs +2 -2
- package/src/Typeahead/BaseTypeahead.doc.mjs +19 -0
- package/src/Typeahead/BaseTypeahead.tsx +5 -2
- package/src/Typeahead/Typeahead.doc.mjs +1 -1
- package/src/Typeahead/Typeahead.test.tsx +45 -1
- package/src/__tests__/TestIcon.tsx +26 -0
- package/src/docPropReferences.test.ts +178 -0
- package/src/hooks/focusableSelector.ts +1 -1
- package/src/hooks/useFocusTrap.doc.mjs +4 -2
- package/src/hooks/useFocusTrap.test.tsx +149 -1
- package/src/hooks/useFocusTrap.ts +50 -1
- package/src/theme/SyntaxTheme.doc.mjs +4 -4
- package/src/theme/defineTheme.ts +4 -13
- package/src/theme/derivedVarRegistry.test.ts +2 -2
- package/src/theme/expandColorScale.test.ts +29 -0
- package/src/theme/expandColorScale.ts +11 -6
- package/src/theme/hct.ts +9 -17
- package/src/theme/tokens.test.ts +182 -9
- package/src/theme/tokens.ts +239 -4
- package/src/theme/useTheme.test.tsx +20 -0
- package/src/utils/color.test.ts +133 -0
- package/src/utils/color.ts +148 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/timeParser.test.ts +13 -0
- package/src/utils/timeParser.ts +8 -5
|
@@ -20,6 +20,12 @@ export const docs = {
|
|
|
20
20
|
description: 'Border on the appropriate edge.',
|
|
21
21
|
default: 'false',
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
name: 'padding',
|
|
25
|
+
type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10',
|
|
26
|
+
description:
|
|
27
|
+
'Internal padding using the spacing scale. Overrides the default padding from the layout container.',
|
|
28
|
+
},
|
|
23
29
|
{
|
|
24
30
|
name: 'isScrollable',
|
|
25
31
|
type: 'boolean',
|
|
@@ -36,6 +42,18 @@ export const docs = {
|
|
|
36
42
|
type: 'AriaRole',
|
|
37
43
|
description: 'ARIA landmark role.',
|
|
38
44
|
},
|
|
45
|
+
{
|
|
46
|
+
name: 'width',
|
|
47
|
+
type: 'number | string',
|
|
48
|
+
description:
|
|
49
|
+
'Width of the panel. Numbers are treated as pixels, strings are used as-is. Ignored when resizable is provided; the hook controls width.',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'resizable',
|
|
53
|
+
type: 'ResizableProps',
|
|
54
|
+
description:
|
|
55
|
+
'Resize props from useResizable(). When provided, the hook drives the panel width and a ResizeHandle should be placed adjacent to the panel.',
|
|
56
|
+
},
|
|
39
57
|
],
|
|
40
58
|
};
|
|
41
59
|
|
|
@@ -56,6 +74,11 @@ export const docsZh = {
|
|
|
56
74
|
description: '相应边缘的边框。',
|
|
57
75
|
default: 'false',
|
|
58
76
|
},
|
|
77
|
+
{
|
|
78
|
+
name: 'padding',
|
|
79
|
+
type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10',
|
|
80
|
+
description: '使用间距比例的内边距。覆盖布局容器的默认内边距。',
|
|
81
|
+
},
|
|
59
82
|
{
|
|
60
83
|
name: 'isScrollable',
|
|
61
84
|
type: 'boolean',
|
|
@@ -72,6 +95,18 @@ export const docsZh = {
|
|
|
72
95
|
type: 'AriaRole',
|
|
73
96
|
description: 'ARIA 地标角色。',
|
|
74
97
|
},
|
|
98
|
+
{
|
|
99
|
+
name: 'width',
|
|
100
|
+
type: 'number | string',
|
|
101
|
+
description:
|
|
102
|
+
'面板宽度。数字按像素处理,字符串按原样使用。提供 resizable 时忽略,宽度由 hook 控制。',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'resizable',
|
|
106
|
+
type: 'ResizableProps',
|
|
107
|
+
description:
|
|
108
|
+
'来自 useResizable() 的调整大小属性。提供时面板宽度由 hook 驱动,应在面板旁放置 ResizeHandle。',
|
|
109
|
+
},
|
|
75
110
|
],
|
|
76
111
|
};
|
|
77
112
|
|
|
@@ -83,8 +118,13 @@ export const docsDense = {
|
|
|
83
118
|
propDescriptions: {
|
|
84
119
|
children: 'Panel content.',
|
|
85
120
|
hasDivider: 'Border on appropriate edge.',
|
|
121
|
+
padding: 'Internal padding on spacing scale. Overrides layout container default.',
|
|
86
122
|
isScrollable: 'Enable scrollable overflow.',
|
|
87
123
|
label: 'Accessible label for landmark element.',
|
|
88
124
|
role: 'ARIA landmark role.',
|
|
125
|
+
width:
|
|
126
|
+
'Panel width. Numbers = pixels, strings as-is. Ignored when resizable provided; hook controls width.',
|
|
127
|
+
resizable:
|
|
128
|
+
'Resize props from useResizable(). Hook drives panel width; place ResizeHandle adjacent.',
|
|
89
129
|
},
|
|
90
130
|
};
|
|
@@ -64,8 +64,13 @@ export const docs = {
|
|
|
64
64
|
{ guidance: false, description: 'Nest interactive content inside captions; keep them plain text.' },
|
|
65
65
|
],
|
|
66
66
|
},
|
|
67
|
+
// The lightbox opens via showModal() and renders nothing while closed —
|
|
68
|
+
// overlay mode gives the Properties preview an open trigger instead of an
|
|
69
|
+
// empty stage, mirroring MobileNav (#3616).
|
|
67
70
|
playground: {
|
|
71
|
+
overlay: true,
|
|
68
72
|
defaults: {
|
|
73
|
+
isOpen: false,
|
|
69
74
|
media: {
|
|
70
75
|
src: 'https://lookaside.facebook.com/assets/astryx/light-scene-horizontal-1.png',
|
|
71
76
|
alt: 'Coastal shoreline with ocean waves',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
-
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
4
|
-
import {render, screen, fireEvent} from '@testing-library/react';
|
|
3
|
+
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
4
|
+
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
5
5
|
import {Lightbox} from './Lightbox';
|
|
6
|
+
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
6
7
|
|
|
7
8
|
// Mock showModal/close for jsdom
|
|
8
9
|
beforeEach(() => {
|
|
@@ -16,6 +17,16 @@ beforeEach(() => {
|
|
|
16
17
|
});
|
|
17
18
|
});
|
|
18
19
|
|
|
20
|
+
// useAnnounce mounts singleton live regions on <body>; reset between tests so
|
|
21
|
+
// stale announcements from one test don't leak into the next.
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
__resetLiveRegionsForTest();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function politeRegion(): HTMLElement | null {
|
|
27
|
+
return document.querySelector('[data-astryx-live-region="polite"]');
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
describe('Lightbox', () => {
|
|
20
31
|
it('renders as a dialog element', () => {
|
|
21
32
|
render(
|
|
@@ -300,6 +311,116 @@ describe('Lightbox', () => {
|
|
|
300
311
|
});
|
|
301
312
|
});
|
|
302
313
|
|
|
314
|
+
describe('screen-reader announcements', () => {
|
|
315
|
+
const media = [
|
|
316
|
+
{src: '/a.jpg', alt: 'Image A', caption: 'First'},
|
|
317
|
+
{src: '/b.jpg', alt: 'Image B', caption: 'Second'},
|
|
318
|
+
{src: '/c.jpg', alt: 'Image C'},
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
it('announces the new image and position when navigating next via button', async () => {
|
|
322
|
+
render(
|
|
323
|
+
<Lightbox
|
|
324
|
+
isOpen={true}
|
|
325
|
+
onOpenChange={() => {}}
|
|
326
|
+
media={media}
|
|
327
|
+
defaultIndex={0}
|
|
328
|
+
/>,
|
|
329
|
+
);
|
|
330
|
+
fireEvent.click(screen.getByLabelText('Next'));
|
|
331
|
+
await waitFor(() => {
|
|
332
|
+
expect(politeRegion()).toHaveTextContent('Image B, 2 of 3');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('announces the new image and position when navigating via arrow keys', async () => {
|
|
337
|
+
render(
|
|
338
|
+
<Lightbox
|
|
339
|
+
isOpen={true}
|
|
340
|
+
onOpenChange={() => {}}
|
|
341
|
+
media={media}
|
|
342
|
+
defaultIndex={1}
|
|
343
|
+
/>,
|
|
344
|
+
);
|
|
345
|
+
const dialog = document.querySelector('dialog')!;
|
|
346
|
+
fireEvent.keyDown(dialog, {key: 'ArrowRight'});
|
|
347
|
+
await waitFor(() => {
|
|
348
|
+
expect(politeRegion()).toHaveTextContent('Image C, 3 of 3');
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('announces the new image and position when navigating prev', async () => {
|
|
353
|
+
render(
|
|
354
|
+
<Lightbox
|
|
355
|
+
isOpen={true}
|
|
356
|
+
onOpenChange={() => {}}
|
|
357
|
+
media={media}
|
|
358
|
+
defaultIndex={2}
|
|
359
|
+
/>,
|
|
360
|
+
);
|
|
361
|
+
fireEvent.click(screen.getByLabelText('Previous'));
|
|
362
|
+
await waitFor(() => {
|
|
363
|
+
expect(politeRegion()).toHaveTextContent('Image B, 2 of 3');
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('falls back to a positional label when the image has no alt', async () => {
|
|
368
|
+
const unlabeled = [
|
|
369
|
+
{src: '/a.jpg', alt: 'Image A'},
|
|
370
|
+
{src: '/b.jpg', alt: ''},
|
|
371
|
+
];
|
|
372
|
+
render(
|
|
373
|
+
<Lightbox
|
|
374
|
+
isOpen={true}
|
|
375
|
+
onOpenChange={() => {}}
|
|
376
|
+
media={unlabeled}
|
|
377
|
+
defaultIndex={0}
|
|
378
|
+
/>,
|
|
379
|
+
);
|
|
380
|
+
fireEvent.click(screen.getByLabelText('Next'));
|
|
381
|
+
await waitFor(() => {
|
|
382
|
+
expect(politeRegion()).toHaveTextContent('Image 2 of 2');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('does not announce on initial open', async () => {
|
|
387
|
+
render(
|
|
388
|
+
<Lightbox
|
|
389
|
+
isOpen={true}
|
|
390
|
+
onOpenChange={() => {}}
|
|
391
|
+
media={media}
|
|
392
|
+
defaultIndex={1}
|
|
393
|
+
/>,
|
|
394
|
+
);
|
|
395
|
+
// Allow any scheduled rAF to flush; nothing should have been announced.
|
|
396
|
+
await new Promise(resolve => requestAnimationFrame(() => resolve(null)));
|
|
397
|
+
// The dialog's aria-label already names the current image on open, so no
|
|
398
|
+
// live region is created (announce is never called).
|
|
399
|
+
expect(politeRegion()).toBeNull();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('does not announce when the lightbox opens at a new index', async () => {
|
|
403
|
+
const {rerender} = render(
|
|
404
|
+
<Lightbox
|
|
405
|
+
isOpen={false}
|
|
406
|
+
onOpenChange={() => {}}
|
|
407
|
+
media={media}
|
|
408
|
+
index={0}
|
|
409
|
+
/>,
|
|
410
|
+
);
|
|
411
|
+
rerender(
|
|
412
|
+
<Lightbox
|
|
413
|
+
isOpen={true}
|
|
414
|
+
onOpenChange={() => {}}
|
|
415
|
+
media={media}
|
|
416
|
+
index={2}
|
|
417
|
+
/>,
|
|
418
|
+
);
|
|
419
|
+
await new Promise(resolve => requestAnimationFrame(() => resolve(null)));
|
|
420
|
+
expect(politeRegion()).toBeNull();
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
303
424
|
describe('video support', () => {
|
|
304
425
|
it('renders a video element when type is video', () => {
|
|
305
426
|
const {container} = render(
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import {
|
|
20
20
|
useCallback,
|
|
21
21
|
useEffect,
|
|
22
|
+
useMemo,
|
|
22
23
|
useRef,
|
|
23
24
|
useState,
|
|
24
25
|
type MouseEvent as ReactMouseEvent,
|
|
@@ -28,6 +29,7 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
28
29
|
import {colorVars, spacingVars, typeScaleVars} from '../theme/tokens.stylex';
|
|
29
30
|
import {Icon} from '../Icon';
|
|
30
31
|
import {IconButton} from '../IconButton';
|
|
32
|
+
import {useAnnounce} from '../hooks/useAnnounce';
|
|
31
33
|
import {useScrollLock} from '../hooks/useScrollLock';
|
|
32
34
|
import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
|
|
33
35
|
import {mergeProps, mergeRefs} from '../utils';
|
|
@@ -306,7 +308,10 @@ export function Lightbox({
|
|
|
306
308
|
const dragStartRef = useRef({x: 0, y: 0, panX: 0, panY: 0});
|
|
307
309
|
|
|
308
310
|
// Resolve current media item
|
|
309
|
-
const mediaArray =
|
|
311
|
+
const mediaArray = useMemo(
|
|
312
|
+
() => (Array.isArray(media) ? media : [media]),
|
|
313
|
+
[media],
|
|
314
|
+
);
|
|
310
315
|
const isGallery = mediaArray.length > 1;
|
|
311
316
|
const currentItem =
|
|
312
317
|
mediaArray.length > 0
|
|
@@ -329,6 +334,29 @@ export function Lightbox({
|
|
|
329
334
|
setPan({x: 0, y: 0});
|
|
330
335
|
}, [index, currentItem?.src]);
|
|
331
336
|
|
|
337
|
+
// Announce gallery navigation to screen readers. Moving between images only
|
|
338
|
+
// updates the visual counter, which is silent to assistive tech, so mirror
|
|
339
|
+
// each change in a polite live region ("<alt>, 3 of 12", or "Image 3 of 12"
|
|
340
|
+
// when the image has no alt). Announce only when the image changes during an
|
|
341
|
+
// already-open session — not on mount, not when opening (even at a new
|
|
342
|
+
// index, since the dialog's aria-label already names the current image), and
|
|
343
|
+
// not on close.
|
|
344
|
+
const announce = useAnnounce();
|
|
345
|
+
const prevIndexRef = useRef(index);
|
|
346
|
+
const wasOpenRef = useRef(isOpen);
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
const indexChanged = prevIndexRef.current !== index;
|
|
349
|
+
const wasOpen = wasOpenRef.current;
|
|
350
|
+
prevIndexRef.current = index;
|
|
351
|
+
wasOpenRef.current = isOpen;
|
|
352
|
+
if (!indexChanged || !isOpen || !wasOpen) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const item = mediaArray[Math.min(index, mediaArray.length - 1)];
|
|
356
|
+
const position = `${index + 1} of ${mediaArray.length}`;
|
|
357
|
+
announce(item?.alt ? `${item.alt}, ${position}` : `Image ${position}`);
|
|
358
|
+
}, [index, isOpen, announce, mediaArray]);
|
|
359
|
+
|
|
332
360
|
// Open/close dialog
|
|
333
361
|
useIsomorphicLayoutEffect(() => {
|
|
334
362
|
const dialog = dialogRef.current;
|
|
@@ -17,12 +17,27 @@ export const docs = {
|
|
|
17
17
|
'NavHeadingMenuItem renders individual selectable items. ' +
|
|
18
18
|
'Pass as the menu prop of SideNavHeading or TopNavHeading.',
|
|
19
19
|
},
|
|
20
|
+
playground: {
|
|
21
|
+
defaults: {
|
|
22
|
+
children: [
|
|
23
|
+
{__element: 'NavHeadingMenuItem', props: {label: 'Dashboard', href: '#'}},
|
|
24
|
+
{__element: 'NavHeadingMenuItem', props: {label: 'Analytics', href: '#'}},
|
|
25
|
+
{__element: 'NavHeadingMenuItem', props: {label: 'Settings', href: '#'}},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
20
29
|
props: [
|
|
21
30
|
{name: 'children', type: 'ReactNode', required: true, description: 'Menu items.'},
|
|
22
31
|
{name: 'size', type: "'sm' | 'md' | 'lg'", default: "'md'", description: 'Controls min-width and item padding.'},
|
|
23
32
|
{name: 'minWidth', type: 'number | string', description: 'Minimum width override.'},
|
|
24
33
|
{name: 'xstyle', type: 'StyleXStyles', description: 'StyleX styles for layout customization (margins, positioning, sizing). Must be a stylex.create() value, not an inline style object like style={{}}.'},
|
|
25
34
|
],
|
|
35
|
+
theming: {
|
|
36
|
+
targets: [
|
|
37
|
+
{className: 'astryx-nav-heading-menu', visualProps: ['size']},
|
|
38
|
+
{className: 'astryx-nav-heading-menu-item', visualProps: ['size']},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
26
41
|
};
|
|
27
42
|
|
|
28
43
|
/** @type {import('../docs-types').TranslationDoc} */
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
13
13
|
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
|
-
import {
|
|
15
|
+
import {TestIcon} from '../__tests__/TestIcon';
|
|
16
16
|
import {NumberInput} from './NumberInput';
|
|
17
17
|
|
|
18
18
|
// Mock showPopover/hidePopover since jsdom does not implement them. Used by the
|
|
@@ -174,7 +174,7 @@ describe('NumberInput', () => {
|
|
|
174
174
|
label="Count"
|
|
175
175
|
value={null}
|
|
176
176
|
onChange={() => {}}
|
|
177
|
-
startIcon={
|
|
177
|
+
startIcon={TestIcon}
|
|
178
178
|
/>,
|
|
179
179
|
);
|
|
180
180
|
expect(screen.getByRole('spinbutton')).toBeInTheDocument();
|
|
@@ -707,7 +707,7 @@ describe('NumberInput', () => {
|
|
|
707
707
|
label="Qty"
|
|
708
708
|
value={0}
|
|
709
709
|
onChange={() => {}}
|
|
710
|
-
startIcon={<
|
|
710
|
+
startIcon={<TestIcon />}
|
|
711
711
|
/>,
|
|
712
712
|
);
|
|
713
713
|
|
|
@@ -867,3 +867,48 @@ describe('NumberInput', () => {
|
|
|
867
867
|
});
|
|
868
868
|
});
|
|
869
869
|
});
|
|
870
|
+
|
|
871
|
+
describe('keyboard clearing with hasClear (#3599)', () => {
|
|
872
|
+
it('commits null when the input is emptied and blurred', () => {
|
|
873
|
+
const onChange = vi.fn();
|
|
874
|
+
render(
|
|
875
|
+
<NumberInput label="Qty" hasClear value={42} onChange={onChange} />,
|
|
876
|
+
);
|
|
877
|
+
const input = screen.getByLabelText('Qty');
|
|
878
|
+
fireEvent.change(input, {target: {value: ''}});
|
|
879
|
+
fireEvent.blur(input);
|
|
880
|
+
expect(onChange).toHaveBeenCalledWith(null);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('commits null when the input is emptied and Enter is pressed', () => {
|
|
884
|
+
const onChange = vi.fn();
|
|
885
|
+
render(
|
|
886
|
+
<NumberInput label="Qty" hasClear value={42} onChange={onChange} />,
|
|
887
|
+
);
|
|
888
|
+
const input = screen.getByLabelText('Qty');
|
|
889
|
+
fireEvent.change(input, {target: {value: ''}});
|
|
890
|
+
fireEvent.keyDown(input, {key: 'Enter'});
|
|
891
|
+
expect(onChange).toHaveBeenCalledWith(null);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('does not fire when emptied and blurred with no prior value', () => {
|
|
895
|
+
const onChange = vi.fn();
|
|
896
|
+
render(
|
|
897
|
+
<NumberInput label="Qty" hasClear value={null} onChange={onChange} />,
|
|
898
|
+
);
|
|
899
|
+
const input = screen.getByLabelText('Qty');
|
|
900
|
+
fireEvent.change(input, {target: {value: ''}});
|
|
901
|
+
fireEvent.blur(input);
|
|
902
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('still reverts on blur when hasClear is not set', () => {
|
|
906
|
+
const onChange = vi.fn();
|
|
907
|
+
render(<NumberInput label="Qty" value={42} onChange={onChange} />);
|
|
908
|
+
const input = screen.getByLabelText('Qty');
|
|
909
|
+
fireEvent.change(input, {target: {value: ''}});
|
|
910
|
+
fireEvent.blur(input);
|
|
911
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
912
|
+
expect((input as HTMLInputElement).value).toBe('42');
|
|
913
|
+
});
|
|
914
|
+
});
|
|
@@ -503,13 +503,21 @@ export function NumberInput({
|
|
|
503
503
|
const handleBlur = useCallback(
|
|
504
504
|
(e: FocusEvent<HTMLInputElement>) => {
|
|
505
505
|
if (pendingInput !== null) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
506
|
+
if (hasClear && pendingInput.trim() === '') {
|
|
507
|
+
// Keyboard clearing honors the clearable contract: an emptied
|
|
508
|
+
// input commits null instead of silently reverting on blur.
|
|
509
|
+
if (value != null) {
|
|
510
|
+
onChange(null);
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
const parsed = parseNumberInput(pendingInput, {
|
|
514
|
+
min,
|
|
515
|
+
max,
|
|
516
|
+
isIntegerOnly,
|
|
517
|
+
});
|
|
518
|
+
if (parsed !== null && parsed !== value) {
|
|
519
|
+
onChange(parsed);
|
|
520
|
+
}
|
|
513
521
|
}
|
|
514
522
|
}
|
|
515
523
|
|
|
@@ -517,7 +525,7 @@ export function NumberInput({
|
|
|
517
525
|
setPendingInput(null);
|
|
518
526
|
onBlur?.(e);
|
|
519
527
|
},
|
|
520
|
-
[pendingInput, value, onChange, min, max, isIntegerOnly, onBlur],
|
|
528
|
+
[pendingInput, value, onChange, min, max, isIntegerOnly, onBlur, hasClear],
|
|
521
529
|
);
|
|
522
530
|
|
|
523
531
|
// Handle keyboard events
|
|
@@ -526,13 +534,21 @@ export function NumberInput({
|
|
|
526
534
|
if (e.key === 'Enter') {
|
|
527
535
|
// Validate and commit on Enter
|
|
528
536
|
if (pendingInput !== null) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
537
|
+
if (hasClear && pendingInput.trim() === '') {
|
|
538
|
+
// Same clearable contract as blur: Enter on an emptied input
|
|
539
|
+
// commits null instead of reverting.
|
|
540
|
+
if (value != null) {
|
|
541
|
+
onChange(null);
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
const parsed = parseNumberInput(pendingInput, {
|
|
545
|
+
min,
|
|
546
|
+
max,
|
|
547
|
+
isIntegerOnly,
|
|
548
|
+
});
|
|
549
|
+
if (parsed !== null && parsed !== value) {
|
|
550
|
+
onChange(parsed);
|
|
551
|
+
}
|
|
536
552
|
}
|
|
537
553
|
}
|
|
538
554
|
onEnter?.();
|
|
@@ -548,6 +564,7 @@ export function NumberInput({
|
|
|
548
564
|
isIntegerOnly,
|
|
549
565
|
onEnter,
|
|
550
566
|
onKeyDown,
|
|
567
|
+
hasClear,
|
|
551
568
|
],
|
|
552
569
|
);
|
|
553
570
|
|
|
@@ -180,6 +180,12 @@ export const docs = {
|
|
|
180
180
|
description: 'Ref forwarded to the overlay root element.',
|
|
181
181
|
},
|
|
182
182
|
],
|
|
183
|
+
theming: {
|
|
184
|
+
targets: [
|
|
185
|
+
{className: 'astryx-overlay'},
|
|
186
|
+
{className: 'astryx-overlay-scrim', visualProps: ['position']},
|
|
187
|
+
],
|
|
188
|
+
},
|
|
183
189
|
};
|
|
184
190
|
|
|
185
191
|
/** @type {import('../docs-types').TranslationDoc} */
|
|
@@ -345,6 +345,149 @@ describe('Pagination', () => {
|
|
|
345
345
|
const activeDot = screen.getByRole('button', {name: 'Go to page 3'});
|
|
346
346
|
expect(activeDot).toHaveAttribute('aria-current', 'page');
|
|
347
347
|
});
|
|
348
|
+
|
|
349
|
+
// -------------------------------------------------------------------------
|
|
350
|
+
// Keyboard navigation (roving tabindex + arrow keys via useListFocus).
|
|
351
|
+
// Selection follows focus: arrow/Home/End move focus and select that page.
|
|
352
|
+
// -------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
it('uses roving tabindex — active dot has tabIndex 0, others -1', () => {
|
|
355
|
+
render(
|
|
356
|
+
<Pagination
|
|
357
|
+
page={2}
|
|
358
|
+
onChange={() => {}}
|
|
359
|
+
totalPages={4}
|
|
360
|
+
variant="dots"
|
|
361
|
+
/>,
|
|
362
|
+
);
|
|
363
|
+
expect(
|
|
364
|
+
screen.getByRole('button', {name: 'Go to page 2'}),
|
|
365
|
+
).toHaveAttribute('tabindex', '0');
|
|
366
|
+
expect(
|
|
367
|
+
screen.getByRole('button', {name: 'Go to page 1'}),
|
|
368
|
+
).toHaveAttribute('tabindex', '-1');
|
|
369
|
+
expect(
|
|
370
|
+
screen.getByRole('button', {name: 'Go to page 3'}),
|
|
371
|
+
).toHaveAttribute('tabindex', '-1');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('ArrowRight moves focus to the next dot and selects it', async () => {
|
|
375
|
+
const user = userEvent.setup();
|
|
376
|
+
const onChange = vi.fn();
|
|
377
|
+
render(
|
|
378
|
+
<Pagination
|
|
379
|
+
page={2}
|
|
380
|
+
onChange={onChange}
|
|
381
|
+
totalPages={4}
|
|
382
|
+
variant="dots"
|
|
383
|
+
/>,
|
|
384
|
+
);
|
|
385
|
+
screen.getByRole('button', {name: 'Go to page 2'}).focus();
|
|
386
|
+
await user.keyboard('{ArrowRight}');
|
|
387
|
+
expect(onChange).toHaveBeenCalledWith(3);
|
|
388
|
+
expect(screen.getByRole('button', {name: 'Go to page 3'})).toHaveFocus();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('ArrowLeft moves focus to the previous dot and selects it', async () => {
|
|
392
|
+
const user = userEvent.setup();
|
|
393
|
+
const onChange = vi.fn();
|
|
394
|
+
render(
|
|
395
|
+
<Pagination
|
|
396
|
+
page={3}
|
|
397
|
+
onChange={onChange}
|
|
398
|
+
totalPages={4}
|
|
399
|
+
variant="dots"
|
|
400
|
+
/>,
|
|
401
|
+
);
|
|
402
|
+
screen.getByRole('button', {name: 'Go to page 3'}).focus();
|
|
403
|
+
await user.keyboard('{ArrowLeft}');
|
|
404
|
+
expect(onChange).toHaveBeenCalledWith(2);
|
|
405
|
+
expect(screen.getByRole('button', {name: 'Go to page 2'})).toHaveFocus();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('Home selects the first page, End the last', async () => {
|
|
409
|
+
const user = userEvent.setup();
|
|
410
|
+
const onChange = vi.fn();
|
|
411
|
+
const {rerender} = render(
|
|
412
|
+
<Pagination
|
|
413
|
+
page={3}
|
|
414
|
+
onChange={onChange}
|
|
415
|
+
totalPages={5}
|
|
416
|
+
variant="dots"
|
|
417
|
+
/>,
|
|
418
|
+
);
|
|
419
|
+
screen.getByRole('button', {name: 'Go to page 3'}).focus();
|
|
420
|
+
await user.keyboard('{Home}');
|
|
421
|
+
expect(onChange).toHaveBeenCalledWith(1);
|
|
422
|
+
expect(screen.getByRole('button', {name: 'Go to page 1'})).toHaveFocus();
|
|
423
|
+
|
|
424
|
+
onChange.mockClear();
|
|
425
|
+
rerender(
|
|
426
|
+
<Pagination
|
|
427
|
+
page={3}
|
|
428
|
+
onChange={onChange}
|
|
429
|
+
totalPages={5}
|
|
430
|
+
variant="dots"
|
|
431
|
+
/>,
|
|
432
|
+
);
|
|
433
|
+
screen.getByRole('button', {name: 'Go to page 3'}).focus();
|
|
434
|
+
await user.keyboard('{End}');
|
|
435
|
+
expect(onChange).toHaveBeenCalledWith(5);
|
|
436
|
+
expect(screen.getByRole('button', {name: 'Go to page 5'})).toHaveFocus();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('wraps from the last dot to the first with ArrowRight', async () => {
|
|
440
|
+
const user = userEvent.setup();
|
|
441
|
+
const onChange = vi.fn();
|
|
442
|
+
render(
|
|
443
|
+
<Pagination
|
|
444
|
+
page={4}
|
|
445
|
+
onChange={onChange}
|
|
446
|
+
totalPages={4}
|
|
447
|
+
variant="dots"
|
|
448
|
+
/>,
|
|
449
|
+
);
|
|
450
|
+
screen.getByRole('button', {name: 'Go to page 4'}).focus();
|
|
451
|
+
await user.keyboard('{ArrowRight}');
|
|
452
|
+
expect(onChange).toHaveBeenCalledWith(1);
|
|
453
|
+
expect(screen.getByRole('button', {name: 'Go to page 1'})).toHaveFocus();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('wraps from the first dot to the last with ArrowLeft', async () => {
|
|
457
|
+
const user = userEvent.setup();
|
|
458
|
+
const onChange = vi.fn();
|
|
459
|
+
render(
|
|
460
|
+
<Pagination
|
|
461
|
+
page={1}
|
|
462
|
+
onChange={onChange}
|
|
463
|
+
totalPages={4}
|
|
464
|
+
variant="dots"
|
|
465
|
+
/>,
|
|
466
|
+
);
|
|
467
|
+
screen.getByRole('button', {name: 'Go to page 1'}).focus();
|
|
468
|
+
await user.keyboard('{ArrowLeft}');
|
|
469
|
+
expect(onChange).toHaveBeenCalledWith(4);
|
|
470
|
+
expect(screen.getByRole('button', {name: 'Go to page 4'})).toHaveFocus();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('does not navigate with arrow keys when disabled', async () => {
|
|
474
|
+
const user = userEvent.setup();
|
|
475
|
+
const onChange = vi.fn();
|
|
476
|
+
render(
|
|
477
|
+
<Pagination
|
|
478
|
+
page={2}
|
|
479
|
+
onChange={onChange}
|
|
480
|
+
totalPages={4}
|
|
481
|
+
variant="dots"
|
|
482
|
+
isDisabled
|
|
483
|
+
/>,
|
|
484
|
+
);
|
|
485
|
+
const dot = screen.getByRole('button', {name: 'Go to page 2'});
|
|
486
|
+
expect(dot).toBeDisabled();
|
|
487
|
+
dot.focus();
|
|
488
|
+
await user.keyboard('{ArrowRight}');
|
|
489
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
490
|
+
});
|
|
348
491
|
});
|
|
349
492
|
|
|
350
493
|
// ---------------------------------------------------------------------------
|