@astryxdesign/core 0.5.0-canary.f4d494c → 0.5.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/Avatar/Avatar.d.ts +1 -25
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +9 -43
- package/dist/Avatar/index.d.ts +2 -2
- package/dist/Avatar/index.d.ts.map +1 -1
- package/dist/Avatar/index.js +1 -1
- package/dist/AvatarGroup/AvatarGroup.d.ts +2 -9
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +2 -5
- package/dist/AvatarGroup/AvatarGroupContext.d.ts +1 -2
- package/dist/AvatarGroup/AvatarGroupContext.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +6 -10
- package/dist/DateInput/DateInput.d.ts +0 -43
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +0 -19
- package/dist/DateInput/index.d.ts +1 -1
- package/dist/DateInput/index.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +3 -52
- package/dist/HoverCard/useHoverCard.d.ts +2 -11
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +0 -2
- package/dist/Indicator/CheckboxIndicator.d.ts.map +1 -1
- package/dist/Indicator/CheckboxIndicator.js +2 -6
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +26 -2
- package/dist/astryx.css +0 -21
- package/dist/hooks/useHotkeys.d.ts.map +1 -1
- package/dist/hooks/useHotkeys.js +25 -3
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +0 -4
- package/dist/theme/syntax/tokens.d.ts +1 -1
- package/dist/theme/syntax/tokens.d.ts.map +1 -1
- package/dist/theme/syntax/tokens.js +2 -5
- package/package.json +3 -3
- package/src/Avatar/Avatar.doc.mjs +2 -12
- package/src/Avatar/Avatar.test.tsx +0 -80
- package/src/Avatar/Avatar.tsx +14 -64
- package/src/Avatar/index.ts +2 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +2 -8
- package/src/AvatarGroup/AvatarGroup.test.tsx +0 -24
- package/src/AvatarGroup/AvatarGroup.tsx +4 -12
- package/src/AvatarGroup/AvatarGroupContext.ts +3 -5
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +4 -10
- package/src/DateInput/DateInput.doc.mjs +0 -16
- package/src/DateInput/DateInput.tsx +0 -52
- package/src/DateInput/DateInputTouch.test.tsx +9 -32
- package/src/DateInput/index.ts +0 -1
- package/src/HoverCard/HoverCard.test.tsx +4 -84
- package/src/HoverCard/HoverCard.tsx +3 -65
- package/src/HoverCard/useHoverCard.tsx +2 -15
- package/src/Indicator/CheckboxIndicator.tsx +8 -14
- package/src/Indicator/Indicator.doc.mjs +0 -2
- package/src/Kbd/Kbd.test.tsx +17 -0
- package/src/Kbd/Kbd.tsx +26 -2
- package/src/hooks/useHotkeys.test.ts +18 -0
- package/src/hooks/useHotkeys.ts +25 -3
- package/src/theme/derivedVarRegistry.test.ts +0 -1
- package/src/theme/derivedVarRegistry.ts +0 -1
- package/src/theme/syntax/tokens.ts +2 -5
- package/dist/DateInput/NativeDateField.d.ts +0 -10
- package/dist/DateInput/NativeDateField.d.ts.map +0 -1
- package/dist/DateInput/NativeDateField.js +0 -477
- package/dist/utils/isApplePlatform.d.ts +0 -18
- package/dist/utils/isApplePlatform.d.ts.map +0 -1
- package/dist/utils/isApplePlatform.js +0 -37
- package/src/DateInput/NativeDateField.test.tsx +0 -565
- package/src/DateInput/NativeDateField.tsx +0 -623
- package/src/utils/isApplePlatform.test.ts +0 -61
- package/src/utils/isApplePlatform.ts +0 -37
package/src/Avatar/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file index.ts
|
|
7
7
|
* @input Imports Avatar component and types from Avatar.tsx, AvatarStatusDot from AvatarStatusDot.tsx
|
|
8
|
-
* @output Exports Avatar, AvatarProps, AvatarSize,
|
|
8
|
+
* @output Exports Avatar, AvatarProps, AvatarSize, AvatarStatusDot, AvatarStatusDotProps, AvatarStatusDotVariant
|
|
9
9
|
* @position Component entry point; re-exported by /packages/core/src/index.ts
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update this header and /packages/core/src/Avatar/Avatar.doc.mjs
|
|
@@ -38,7 +38,7 @@ export interface AvatarStatusDotVariantMap {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export {Avatar, resolveSize} from './Avatar';
|
|
41
|
-
export type {AvatarProps, AvatarSize
|
|
41
|
+
export type {AvatarProps, AvatarSize} from './Avatar';
|
|
42
42
|
export {AvatarStatusDot} from './AvatarStatusDot';
|
|
43
43
|
export type {
|
|
44
44
|
AvatarStatusDotProps,
|
|
@@ -25,8 +25,8 @@ export const docs = {
|
|
|
25
25
|
},
|
|
26
26
|
theming: {
|
|
27
27
|
targets: [
|
|
28
|
-
{className: 'astryx-avatar-group', visualProps: ['size'
|
|
29
|
-
{className: 'astryx-avatar-group-overflow', visualProps: ['size'
|
|
28
|
+
{className: 'astryx-avatar-group', visualProps: ['size']},
|
|
29
|
+
{className: 'astryx-avatar-group-overflow', visualProps: ['size']},
|
|
30
30
|
],
|
|
31
31
|
},
|
|
32
32
|
description: 'Stacked avatar display with overlapping layout and optional overflow indicator. Children are Avatar elements.',
|
|
@@ -52,12 +52,6 @@ export const docs = {
|
|
|
52
52
|
"Size applied to all avatars via context. This wins over each child Avatar's own size prop, including when it is left at the default, so set the size here rather than on the children.",
|
|
53
53
|
default: "'md'",
|
|
54
54
|
},
|
|
55
|
-
{
|
|
56
|
-
name: 'shape',
|
|
57
|
-
type: "'circle' | 'rounded' | 'square'",
|
|
58
|
-
description: "Shape applied to all avatars via context, overriding each avatar's own shape prop. Also applied to AvatarGroupOverflow's \"+N\" indicator.",
|
|
59
|
-
default: "'circle'",
|
|
60
|
-
},
|
|
61
55
|
{
|
|
62
56
|
name: 'ref',
|
|
63
57
|
type: 'React.Ref<HTMLDivElement>',
|
|
@@ -118,30 +118,6 @@ describe('AvatarGroupOverflow', () => {
|
|
|
118
118
|
expect(overflow).toHaveAttribute('data-size', 'lg');
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
it('applies the group shape to the overflow chip, matching its avatars', () => {
|
|
122
|
-
render(
|
|
123
|
-
<AvatarGroup shape="square">
|
|
124
|
-
<Avatar name="Alice" />
|
|
125
|
-
<AvatarGroupOverflow count={5} />
|
|
126
|
-
</AvatarGroup>,
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const overflow = screen.getByLabelText('5 more');
|
|
130
|
-
expect(overflow).toHaveAttribute('data-shape', 'square');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('defaults the overflow chip to circle shape with no explicit AvatarGroup shape', () => {
|
|
134
|
-
render(
|
|
135
|
-
<AvatarGroup>
|
|
136
|
-
<Avatar name="Alice" />
|
|
137
|
-
<AvatarGroupOverflow count={5} />
|
|
138
|
-
</AvatarGroup>,
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const overflow = screen.getByLabelText('5 more');
|
|
142
|
-
expect(overflow).toHaveAttribute('data-shape', 'circle');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
121
|
it('renders as button when onClick is provided', () => {
|
|
146
122
|
render(
|
|
147
123
|
<AvatarGroup>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import {useId, useMemo, useState, type ReactNode} from 'react';
|
|
28
28
|
import type {BaseProps} from '../BaseProps';
|
|
29
|
-
import {resolveSize, type
|
|
29
|
+
import {resolveSize, type AvatarSize} from '../Avatar';
|
|
30
30
|
import * as stylex from '@stylexjs/stylex';
|
|
31
31
|
import {mergeProps} from '../utils';
|
|
32
32
|
import {composeEventHandlers} from '../utils/composeEventHandlers';
|
|
@@ -55,13 +55,6 @@ export interface AvatarGroupProps extends BaseProps<HTMLDivElement> {
|
|
|
55
55
|
* @default 'md'
|
|
56
56
|
*/
|
|
57
57
|
size?: AvatarSize;
|
|
58
|
-
/**
|
|
59
|
-
* Shape applied to all avatars via context, overriding each avatar's own
|
|
60
|
-
* `shape` prop so a group stays visually uniform. Also applied to the
|
|
61
|
-
* `AvatarGroupOverflow` "+N" indicator, so it matches the group.
|
|
62
|
-
* @default 'circle'
|
|
63
|
-
*/
|
|
64
|
-
shape?: AvatarShape;
|
|
65
58
|
/**
|
|
66
59
|
* Test ID for integration testing.
|
|
67
60
|
*/
|
|
@@ -96,7 +89,6 @@ const styles = stylex.create({
|
|
|
96
89
|
export function AvatarGroup({
|
|
97
90
|
children,
|
|
98
91
|
size = 'md',
|
|
99
|
-
shape = 'circle',
|
|
100
92
|
'data-testid': testId,
|
|
101
93
|
'aria-label': ariaLabelFromProps,
|
|
102
94
|
'aria-describedby': ariaDescribedByFromProps,
|
|
@@ -114,8 +106,8 @@ export function AvatarGroup({
|
|
|
114
106
|
const overlap = Math.round(numericSize * OVERLAP_RATIO);
|
|
115
107
|
|
|
116
108
|
const contextValue = useMemo(
|
|
117
|
-
() => ({size,
|
|
118
|
-
[size,
|
|
109
|
+
() => ({size, overlap, numericSize}),
|
|
110
|
+
[size, overlap, numericSize],
|
|
119
111
|
);
|
|
120
112
|
|
|
121
113
|
// The keyboard hint and roving tab stop only make sense once the group has
|
|
@@ -160,7 +152,7 @@ export function AvatarGroup({
|
|
|
160
152
|
onKeyDown={composeEventHandlers(onKeyDown, handleKeyDown)}
|
|
161
153
|
onFocus={composeEventHandlers(onFocus, handleFocus)}
|
|
162
154
|
{...mergeProps(
|
|
163
|
-
themeProps('avatar-group', {size
|
|
155
|
+
themeProps('avatar-group', {size}),
|
|
164
156
|
stylex.props(styles.root, xstyle),
|
|
165
157
|
className,
|
|
166
158
|
style,
|
|
@@ -9,18 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {createContext, use} from 'react';
|
|
12
|
-
import type {
|
|
12
|
+
import type {AvatarSize} from '../Avatar';
|
|
13
13
|
|
|
14
14
|
export interface AvatarGroupContextValue {
|
|
15
15
|
size: AvatarSize;
|
|
16
|
-
shape: AvatarShape;
|
|
17
16
|
overlap: number;
|
|
18
17
|
numericSize: number;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
export const AvatarGroupContext =
|
|
22
|
-
null
|
|
23
|
-
);
|
|
20
|
+
export const AvatarGroupContext =
|
|
21
|
+
createContext<AvatarGroupContextValue | null>(null);
|
|
24
22
|
AvatarGroupContext.displayName = 'AvatarGroupContext';
|
|
25
23
|
|
|
26
24
|
export function useAvatarGroup(): AvatarGroupContextValue | null {
|
|
@@ -20,9 +20,9 @@ import {
|
|
|
20
20
|
typographyVars,
|
|
21
21
|
typeScaleVars,
|
|
22
22
|
fontWeightVars,
|
|
23
|
+
radiusVars,
|
|
23
24
|
spacingVars,
|
|
24
25
|
} from '../theme/tokens.stylex';
|
|
25
|
-
import {shapeStyles} from '../Avatar/Avatar';
|
|
26
26
|
import {mergeProps} from '../utils';
|
|
27
27
|
import {resolveSize} from '../Avatar';
|
|
28
28
|
import {useAvatarGroup} from './AvatarGroupContext';
|
|
@@ -65,10 +65,7 @@ const styles = stylex.create({
|
|
|
65
65
|
display: 'inline-flex',
|
|
66
66
|
alignItems: 'center',
|
|
67
67
|
justifyContent: 'center',
|
|
68
|
-
|
|
69
|
-
// shared with Avatar) so the overflow indicator matches the group's
|
|
70
|
-
// shape instead of always staying a circle.
|
|
71
|
-
borderRadius: 'var(--_avatar-radius)',
|
|
68
|
+
borderRadius: radiusVars['--radius-full'],
|
|
72
69
|
// Use opaque background to prevent avatar bleed-through
|
|
73
70
|
backgroundColor: colorVars['--color-background-surface'],
|
|
74
71
|
color: colorVars['--color-text-secondary'],
|
|
@@ -169,7 +166,6 @@ export function AvatarGroupOverflow({
|
|
|
169
166
|
const t = useTranslator();
|
|
170
167
|
const group = useAvatarGroup();
|
|
171
168
|
const size = group?.size ?? 'md';
|
|
172
|
-
const shape = group?.shape ?? 'circle';
|
|
173
169
|
const numericSize = group?.numericSize ?? resolveSize('md');
|
|
174
170
|
const overlap = group?.overlap ?? 0;
|
|
175
171
|
|
|
@@ -190,7 +186,7 @@ export function AvatarGroupOverflow({
|
|
|
190
186
|
aria-label={label}
|
|
191
187
|
data-avatar-item=""
|
|
192
188
|
{...mergeProps(
|
|
193
|
-
themeProps('avatar-group-overflow', {size
|
|
189
|
+
themeProps('avatar-group-overflow', {size}),
|
|
194
190
|
focusOutlineProps.focusVisible(
|
|
195
191
|
styles.base,
|
|
196
192
|
styles.button,
|
|
@@ -198,7 +194,6 @@ export function AvatarGroupOverflow({
|
|
|
198
194
|
dynamicStyles.size(numericSize),
|
|
199
195
|
dynamicStyles.fontSize(numericSize),
|
|
200
196
|
dynamicStyles.overlap(-overlap),
|
|
201
|
-
shapeStyles[shape],
|
|
202
197
|
xstyle,
|
|
203
198
|
),
|
|
204
199
|
className,
|
|
@@ -215,14 +210,13 @@ export function AvatarGroupOverflow({
|
|
|
215
210
|
{...rest}
|
|
216
211
|
aria-label={label}
|
|
217
212
|
{...mergeProps(
|
|
218
|
-
themeProps('avatar-group-overflow', {size
|
|
213
|
+
themeProps('avatar-group-overflow', {size}),
|
|
219
214
|
stylex.props(
|
|
220
215
|
styles.base,
|
|
221
216
|
styles.overlap,
|
|
222
217
|
dynamicStyles.size(numericSize),
|
|
223
218
|
dynamicStyles.fontSize(numericSize),
|
|
224
219
|
dynamicStyles.overlap(-overlap),
|
|
225
|
-
shapeStyles[shape],
|
|
226
220
|
xstyle,
|
|
227
221
|
),
|
|
228
222
|
className,
|
|
@@ -156,13 +156,6 @@ export const docs = {
|
|
|
156
156
|
"How the committed date value is displayed. Named values are reused from Timestamp's format vocabulary: 'date' shows 'Mar 21, 2026', 'date_long' shows 'March 21, 2026', 'date_weekday' shows 'Wed, Mar 21, 2026', 'system_date' shows '2026-03-21'. A function receives the ISO value and returns a custom string. Applies only to the committed value, never to text being typed.",
|
|
157
157
|
default: "'date_long'",
|
|
158
158
|
},
|
|
159
|
-
{
|
|
160
|
-
name: 'nativePicker',
|
|
161
|
-
type: "'touch' | 'always' | 'never'",
|
|
162
|
-
description:
|
|
163
|
-
"Which surface draws the date picker. 'touch' (the default) hands a touch device to the browser/OS \u2014 the field becomes an input type=date and the platform draws the picker (the iOS wheel, the Android calendar dialog); 'always' does that wherever the browser supports input type=date; 'never' keeps Astryx's own pickers everywhere (the bottom-sheet picker on a finger, the calendar popover on a mouse). Use 'never' for a field that needs weekStartsOn, numberOfMonths or dateConstraints, none of which a native picker can express. format and placeholder still apply in native mode; min and max are forwarded, but a native picker may not show them (on iOS an out-of-range date can be selected and is refused on commit rather than greyed out).",
|
|
164
|
-
default: "'touch'",
|
|
165
|
-
},
|
|
166
159
|
{
|
|
167
160
|
name: 'width',
|
|
168
161
|
type: 'SizeValue',
|
|
@@ -444,13 +437,6 @@ export const docsZh = {
|
|
|
444
437
|
"已选日期的显示格式。命名值复用 Timestamp 的格式词汇:'date' 显示 'Mar 21, 2026','date_long' 显示 'March 21, 2026','date_weekday' 显示 'Wed, Mar 21, 2026','system_date' 显示 '2026-03-21'。函数接收 ISO 值并返回自定义字符串。仅作用于已提交的值,不影响正在输入的文本。",
|
|
445
438
|
default: "'date_long'",
|
|
446
439
|
},
|
|
447
|
-
{
|
|
448
|
-
name: 'nativePicker',
|
|
449
|
-
type: "'touch' | 'always' | 'never'",
|
|
450
|
-
description:
|
|
451
|
-
"由哪个界面绘制日期选择器。'touch'(默认)在触摸设备上交给浏览器/操作系统:字段变为 input type=date,由平台绘制选择器(iOS 滚轮、Android 日历对话框);'always' 在所有支持 input type=date 的浏览器上都这样做;'never' 始终使用 Astryx 自带的选择器(触摸设备用底部弹出选择器,鼠标设备用日历弹出层)。需要 weekStartsOn、numberOfMonths 或 dateConstraints 的字段应使用 'never',原生选择器无法表达这些。原生模式下 format 和 placeholder 仍然生效;min 和 max 会传递给原生控件,但原生选择器可能不会显示这些限制(在 iOS 上仍可选中超出范围的日期,会在提交时被拒绝,而不是变灰)。",
|
|
452
|
-
default: "'touch'",
|
|
453
|
-
},
|
|
454
440
|
{
|
|
455
441
|
name: 'xstyle',
|
|
456
442
|
type: 'StyleXStyles',
|
|
@@ -551,8 +537,6 @@ export const docsDense = {
|
|
|
551
537
|
weekStartsOn: 'first day of week in calendar (0=Sunday, or name e.g. "mon")',
|
|
552
538
|
format:
|
|
553
539
|
"committed-value display: 'date_long' (default, March 21, 2026), 'date' (Mar 21, 2026), 'date_weekday' (Wed, Mar 21, 2026), 'system_date' (2026-03-21), or (iso)=>string; reuses Timestamp vocabulary. Committed value only, not while typing.",
|
|
554
|
-
nativePicker:
|
|
555
|
-
"which surface draws the picker: 'touch' (default) = browser/OS on a coarse pointer, 'always', 'never' = Astryx's own everywhere. use 'never' for weekStartsOn/numberOfMonths/dateConstraints. format+placeholder still apply; min/max forwarded but not necessarily shown by the OS picker \u2014 refused on commit instead.",
|
|
556
540
|
xstyle: 'StyleX styles for layout; must be stylex.create() value',
|
|
557
541
|
},
|
|
558
542
|
};
|
|
@@ -62,7 +62,6 @@ import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
|
62
62
|
import {useMediaQuery} from '../hooks/useMediaQuery';
|
|
63
63
|
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
64
64
|
import {usePopover} from '../Popover';
|
|
65
|
-
import {NativeDateField} from './NativeDateField';
|
|
66
65
|
import {TouchDateField} from './TouchDateField';
|
|
67
66
|
import {useTooltip} from '../Tooltip';
|
|
68
67
|
import {getInputARIA, isImeKeyEvent, parseDateInput} from '../utils';
|
|
@@ -151,17 +150,6 @@ export type DateInputSize = keyof typeof sizeStyles;
|
|
|
151
150
|
* types stay in compile-time lockstep: renaming or removing one of these
|
|
152
151
|
* members from `TimestampFormat` breaks this type at build time.
|
|
153
152
|
*/
|
|
154
|
-
/**
|
|
155
|
-
* When DateInput hands date picking to the browser/OS instead of its own
|
|
156
|
-
* surfaces.
|
|
157
|
-
*
|
|
158
|
-
* - `'touch'`: native on touch devices (coarse pointer), Astryx's calendar
|
|
159
|
-
* popover on mouse-driven ones
|
|
160
|
-
* - `'always'`: native wherever the browser supports `<input type="date">`
|
|
161
|
-
* - `'never'`: Astryx's own pickers everywhere
|
|
162
|
-
*/
|
|
163
|
-
export type DateInputNativePicker = 'touch' | 'always' | 'never';
|
|
164
|
-
|
|
165
153
|
export type DateInputFormat = Extract<
|
|
166
154
|
TimestampFormat,
|
|
167
155
|
'date' | 'date_long' | 'date_weekday' | 'system_date'
|
|
@@ -373,40 +361,6 @@ export interface DateInputProps extends Omit<
|
|
|
373
361
|
* ```
|
|
374
362
|
*/
|
|
375
363
|
format?: DateInputFormat | ((value: ISODateString) => string);
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* When date picking is handed to the browser/OS instead of Astryx's own
|
|
379
|
-
* surfaces: the field becomes an `<input type="date">` and the platform
|
|
380
|
-
* draws the picker — the iOS wheel, the Android calendar dialog — with the
|
|
381
|
-
* OS's own hit areas, momentum scrolling, locale and accessibility
|
|
382
|
-
* settings.
|
|
383
|
-
*
|
|
384
|
-
* - `'touch'` (default): native on touch devices (coarse pointer), the text
|
|
385
|
-
* field and calendar popover on mouse-driven ones
|
|
386
|
-
* - `'always'`: native wherever the browser supports `<input type="date">`
|
|
387
|
-
* - `'never'`: Astryx's own pickers everywhere — the touch picker on a
|
|
388
|
-
* finger, the calendar popover on a mouse
|
|
389
|
-
*
|
|
390
|
-
* `format` and `placeholder` still apply in native mode: DateInput paints
|
|
391
|
-
* the closed field's text itself, over the control. `numberOfMonths` and
|
|
392
|
-
* `weekStartsOn` do not — they describe a calendar grid the native picker
|
|
393
|
-
* does not have — so a field that needs either should pass `'never'`.
|
|
394
|
-
*
|
|
395
|
-
* `min` and `max` are forwarded, but note that a native picker may not
|
|
396
|
-
* *show* them: on iOS they are constraint-validation flags rather than
|
|
397
|
-
* clamps, so an out-of-range date can be selected and is refused on commit
|
|
398
|
-
* (announced to assistive technology) rather than being greyed out in the
|
|
399
|
-
* picker. `dateConstraints` is enforced the same way, on commit, and is
|
|
400
|
-
* reason enough to prefer `'never'` on a field that uses it.
|
|
401
|
-
*
|
|
402
|
-
* @default 'touch'
|
|
403
|
-
* @example
|
|
404
|
-
* ```
|
|
405
|
-
* // Astryx's own touch picker instead of the platform's
|
|
406
|
-
* <DateInput label="Event date" value={date} onChange={setDate} nativePicker="never" />
|
|
407
|
-
* ```
|
|
408
|
-
*/
|
|
409
|
-
nativePicker?: DateInputNativePicker;
|
|
410
364
|
}
|
|
411
365
|
|
|
412
366
|
/**
|
|
@@ -933,13 +887,7 @@ PointerDateField.displayName = 'PointerDateField';
|
|
|
933
887
|
*/
|
|
934
888
|
export function DateInput(props: DateInputProps) {
|
|
935
889
|
const isTouch = useMediaQuery(TOUCH_POINTER_QUERY);
|
|
936
|
-
const nativePicker = props.nativePicker ?? 'touch';
|
|
937
890
|
|
|
938
|
-
// The platform's picker, where the consumer asked for it — see the
|
|
939
|
-
// `nativePicker` prop for what that trades away.
|
|
940
|
-
if (nativePicker === 'always' || (nativePicker === 'touch' && isTouch)) {
|
|
941
|
-
return <NativeDateField {...props} />;
|
|
942
|
-
}
|
|
943
891
|
return isTouch ? (
|
|
944
892
|
<TouchDateField {...props} />
|
|
945
893
|
) : (
|
|
@@ -219,11 +219,6 @@ function Controlled({
|
|
|
219
219
|
return (
|
|
220
220
|
<DateInput
|
|
221
221
|
label="Event date"
|
|
222
|
-
// This suite is about Astryx's own touch surface, which is now opt-out:
|
|
223
|
-
// `nativePicker` defaults to 'touch', so a coarse pointer gets the
|
|
224
|
-
// platform's picker unless a field says otherwise. Tests that assert on
|
|
225
|
-
// the surface selection itself pass their own value over this.
|
|
226
|
-
nativePicker="never"
|
|
227
222
|
{...DEFAULT_RANGE}
|
|
228
223
|
{...props}
|
|
229
224
|
value={value}
|
|
@@ -281,16 +276,6 @@ function weekdayRow(): HTMLElement {
|
|
|
281
276
|
// ---------------------------------------------------------------------------
|
|
282
277
|
|
|
283
278
|
describe('DateInput — surface selection', () => {
|
|
284
|
-
it('hands a touch device to the platform picker by default', () => {
|
|
285
|
-
// `nativePicker` defaults to 'touch': the OS draws the picker unless a
|
|
286
|
-
// field opts out. Everything else in this file passes 'never', so this is
|
|
287
|
-
// the one place the default itself is asserted.
|
|
288
|
-
stubMedia({pointer: 'coarse', width: 393});
|
|
289
|
-
render(<DateInput label="Event date" onChange={() => {}} />);
|
|
290
|
-
const input = document.querySelector('input');
|
|
291
|
-
expect(input).toHaveAttribute('type', 'date');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
279
|
it('switches on the pointer alone, so a tablet gets the picker too', () => {
|
|
295
280
|
// `pointer` is the PRIMARY device, which is what makes it the whole test:
|
|
296
281
|
// a touchscreen laptop reports `fine` (its trackpad) and keeps the
|
|
@@ -335,7 +320,7 @@ describe('DateInput — surface selection', () => {
|
|
|
335
320
|
setViewport('desktop');
|
|
336
321
|
const onChange = vi.fn();
|
|
337
322
|
render(
|
|
338
|
-
<DateInput
|
|
323
|
+
<DateInput label="Event date" onChange={onChange} min={undefined} />,
|
|
339
324
|
);
|
|
340
325
|
fireEvent.change(field(), {target: {value: '2026-03-25'}});
|
|
341
326
|
expect(onChange).toHaveBeenCalledWith('2026-03-25');
|
|
@@ -375,12 +360,12 @@ describe('DateInput — surface selection', () => {
|
|
|
375
360
|
describe('DateInput — field parity', () => {
|
|
376
361
|
it('shows a placeholder until a date is chosen, then the formatted value', () => {
|
|
377
362
|
const {rerender} = render(
|
|
378
|
-
<DateInput
|
|
363
|
+
<DateInput label="Ship date" onChange={() => {}} />,
|
|
379
364
|
);
|
|
380
365
|
expect(field()).toHaveValue('');
|
|
381
366
|
expect(field()).toHaveAttribute('placeholder', 'Select a date');
|
|
382
367
|
rerender(
|
|
383
|
-
<DateInput
|
|
368
|
+
<DateInput label="Ship date" value="2026-03-21" onChange={() => {}} />,
|
|
384
369
|
);
|
|
385
370
|
expect(field()).toHaveValue('March 21, 2026');
|
|
386
371
|
});
|
|
@@ -388,7 +373,6 @@ describe('DateInput — field parity', () => {
|
|
|
388
373
|
it('honors a named format', () => {
|
|
389
374
|
render(
|
|
390
375
|
<DateInput
|
|
391
|
-
nativePicker="never"
|
|
392
376
|
label="Ship date"
|
|
393
377
|
value="2026-03-21"
|
|
394
378
|
format="system_date"
|
|
@@ -401,7 +385,6 @@ describe('DateInput — field parity', () => {
|
|
|
401
385
|
it('honors a function format', () => {
|
|
402
386
|
render(
|
|
403
387
|
<DateInput
|
|
404
|
-
nativePicker="never"
|
|
405
388
|
label="Ship date"
|
|
406
389
|
value="2026-03-21"
|
|
407
390
|
format={iso => `ISO:${iso}`}
|
|
@@ -414,7 +397,6 @@ describe('DateInput — field parity', () => {
|
|
|
414
397
|
it('honors a custom placeholder', () => {
|
|
415
398
|
render(
|
|
416
399
|
<DateInput
|
|
417
|
-
nativePicker="never"
|
|
418
400
|
label="Ship date"
|
|
419
401
|
placeholder="Pick a day"
|
|
420
402
|
onChange={() => {}}
|
|
@@ -427,7 +409,6 @@ describe('DateInput — field parity', () => {
|
|
|
427
409
|
const onChange = vi.fn();
|
|
428
410
|
render(
|
|
429
411
|
<DateInput
|
|
430
|
-
nativePicker="never"
|
|
431
412
|
label="Ship date"
|
|
432
413
|
value="2026-03-21"
|
|
433
414
|
hasClear
|
|
@@ -451,7 +432,6 @@ describe('DateInput — field parity', () => {
|
|
|
451
432
|
try {
|
|
452
433
|
render(
|
|
453
434
|
<DateInput
|
|
454
|
-
nativePicker="never"
|
|
455
435
|
label="Ship date"
|
|
456
436
|
value="2026-03-21"
|
|
457
437
|
hasClear
|
|
@@ -477,7 +457,7 @@ describe('DateInput — field parity', () => {
|
|
|
477
457
|
|
|
478
458
|
it('does not open the picker until the field is tapped', () => {
|
|
479
459
|
withLayout(() => {
|
|
480
|
-
render(<DateInput
|
|
460
|
+
render(<DateInput label="Ship date" onChange={() => {}} />);
|
|
481
461
|
expect(field()).toHaveAttribute('aria-expanded', 'false');
|
|
482
462
|
expect(screen.queryByRole('grid')).not.toBeInTheDocument();
|
|
483
463
|
fireEvent.click(field());
|
|
@@ -495,7 +475,7 @@ describe('DateInput — field parity', () => {
|
|
|
495
475
|
});
|
|
496
476
|
|
|
497
477
|
it('is not openable while disabled', () => {
|
|
498
|
-
render(<DateInput
|
|
478
|
+
render(<DateInput label="Ship date" isDisabled onChange={() => {}} />);
|
|
499
479
|
expect(field()).toBeDisabled();
|
|
500
480
|
fireEvent.click(field());
|
|
501
481
|
expect(screen.queryByRole('grid')).not.toBeInTheDocument();
|
|
@@ -504,7 +484,6 @@ describe('DateInput — field parity', () => {
|
|
|
504
484
|
it('stays focusable and explains itself when disabled with a reason', () => {
|
|
505
485
|
render(
|
|
506
486
|
<DateInput
|
|
507
|
-
nativePicker="never"
|
|
508
487
|
label="Ship date"
|
|
509
488
|
isDisabled
|
|
510
489
|
disabledMessage="You need the Editor role"
|
|
@@ -521,7 +500,6 @@ describe('DateInput — field parity', () => {
|
|
|
521
500
|
it('renders label, description and status through Field', () => {
|
|
522
501
|
render(
|
|
523
502
|
<DateInput
|
|
524
|
-
nativePicker="never"
|
|
525
503
|
label="Ship date"
|
|
526
504
|
description="When it leaves the warehouse"
|
|
527
505
|
status={{type: 'error', message: 'Pick a date'}}
|
|
@@ -538,12 +516,12 @@ describe('DateInput — field parity', () => {
|
|
|
538
516
|
});
|
|
539
517
|
|
|
540
518
|
it('marks required for assistive technology', () => {
|
|
541
|
-
render(<DateInput
|
|
519
|
+
render(<DateInput label="Ship date" isRequired onChange={() => {}} />);
|
|
542
520
|
expect(field()).toHaveAttribute('aria-required', 'true');
|
|
543
521
|
});
|
|
544
522
|
|
|
545
523
|
it('associates the label natively, so the field is named without ARIA', () => {
|
|
546
|
-
render(<DateInput
|
|
524
|
+
render(<DateInput label="Ship date" onChange={() => {}} />);
|
|
547
525
|
expect(screen.getByLabelText('Ship date')).toBe(field());
|
|
548
526
|
});
|
|
549
527
|
|
|
@@ -558,7 +536,6 @@ describe('DateInput — field parity', () => {
|
|
|
558
536
|
withLayout(() => {
|
|
559
537
|
render(
|
|
560
538
|
<DateInput
|
|
561
|
-
nativePicker="never"
|
|
562
539
|
label="Ship date"
|
|
563
540
|
value="2026-03-10"
|
|
564
541
|
min="2026-03-01"
|
|
@@ -581,7 +558,7 @@ describe('DateInput — field parity', () => {
|
|
|
581
558
|
it('drops the Field wrapper inside an InputGroup', () => {
|
|
582
559
|
render(
|
|
583
560
|
<InputGroup label="Range">
|
|
584
|
-
<DateInput
|
|
561
|
+
<DateInput label="Start" onChange={() => {}} />
|
|
585
562
|
</InputGroup>,
|
|
586
563
|
);
|
|
587
564
|
// Named by the group label plus its own, the way core's inputs are.
|
|
@@ -666,7 +643,7 @@ describe('DateInput — calendar surface', () => {
|
|
|
666
643
|
it('Save closes even with no date chosen, committing nothing', () => {
|
|
667
644
|
const onChange = vi.fn();
|
|
668
645
|
withLayout(() => {
|
|
669
|
-
render(<DateInput
|
|
646
|
+
render(<DateInput label="Ship date" onChange={onChange} />);
|
|
670
647
|
fireEvent.click(field());
|
|
671
648
|
fireEvent.click(screen.getByRole('button', {name: 'Save'}));
|
|
672
649
|
});
|
package/src/DateInput/index.ts
CHANGED
|
@@ -365,42 +365,7 @@ describe('HoverCard', () => {
|
|
|
365
365
|
);
|
|
366
366
|
});
|
|
367
367
|
|
|
368
|
-
it('
|
|
369
|
-
render(
|
|
370
|
-
<HoverCard content={<span>Card content</span>} label="Profile actions">
|
|
371
|
-
<button type="button">Trigger</button>
|
|
372
|
-
</HoverCard>,
|
|
373
|
-
);
|
|
374
|
-
const trigger = screen.getByRole('button', {name: 'Trigger'});
|
|
375
|
-
expect(trigger).toHaveAttribute('aria-haspopup', 'dialog');
|
|
376
|
-
// While closed, the layer is not in the DOM, so aria-controls must not
|
|
377
|
-
// point at a missing id (see DateInput). It is set once the card opens.
|
|
378
|
-
expect(trigger).not.toHaveAttribute('aria-controls');
|
|
379
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
380
|
-
expect(trigger).not.toHaveAttribute('aria-describedby');
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('merges existing popup attributes on the trigger when labelled', () => {
|
|
384
|
-
render(
|
|
385
|
-
<HoverCard content={<span>Card content</span>} label="Profile actions">
|
|
386
|
-
<button
|
|
387
|
-
type="button"
|
|
388
|
-
aria-haspopup="menu"
|
|
389
|
-
aria-controls="menu-id"
|
|
390
|
-
aria-expanded="true">
|
|
391
|
-
Trigger
|
|
392
|
-
</button>
|
|
393
|
-
</HoverCard>,
|
|
394
|
-
);
|
|
395
|
-
const trigger = screen.getByRole('button', {name: 'Trigger'});
|
|
396
|
-
// The hover card's dialog popup is advertised, but the trigger's own
|
|
397
|
-
// popup semantics are preserved rather than overwritten.
|
|
398
|
-
expect(trigger).toHaveAttribute('aria-haspopup', 'dialog');
|
|
399
|
-
expect(trigger.getAttribute('aria-controls')).toContain('menu-id');
|
|
400
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it('keeps aria-describedby on the trigger when no label is provided', () => {
|
|
368
|
+
it('injects aria-describedby on trigger', () => {
|
|
404
369
|
render(
|
|
405
370
|
<HoverCard content={<span>Card content</span>}>
|
|
406
371
|
<button type="button">Trigger</button>
|
|
@@ -408,10 +373,9 @@ describe('HoverCard', () => {
|
|
|
408
373
|
);
|
|
409
374
|
const trigger = screen.getByRole('button', {name: 'Trigger'});
|
|
410
375
|
expect(trigger).toHaveAttribute('aria-describedby');
|
|
411
|
-
expect(trigger).not.toHaveAttribute('aria-haspopup');
|
|
412
376
|
});
|
|
413
377
|
|
|
414
|
-
it('
|
|
378
|
+
it('merges existing aria-describedby', () => {
|
|
415
379
|
render(
|
|
416
380
|
<HoverCard content={<span>Card content</span>}>
|
|
417
381
|
<button type="button" aria-describedby="existing-id">
|
|
@@ -424,34 +388,6 @@ describe('HoverCard', () => {
|
|
|
424
388
|
expect(describedBy).toContain('existing-id');
|
|
425
389
|
});
|
|
426
390
|
|
|
427
|
-
it('updates aria-expanded when the labelled hover card opens and closes', async () => {
|
|
428
|
-
render(
|
|
429
|
-
<HoverCard
|
|
430
|
-
content={<span>Card content</span>}
|
|
431
|
-
label="Profile actions"
|
|
432
|
-
delay={0}>
|
|
433
|
-
<button type="button">Trigger</button>
|
|
434
|
-
</HoverCard>,
|
|
435
|
-
);
|
|
436
|
-
const trigger = screen.getByRole('button', {name: 'Trigger'});
|
|
437
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
438
|
-
expect(trigger).not.toHaveAttribute('aria-controls');
|
|
439
|
-
|
|
440
|
-
fireEvent.mouseEnter(trigger);
|
|
441
|
-
await waitFor(() => {
|
|
442
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
443
|
-
// The layer is in the DOM now, so aria-controls points at it.
|
|
444
|
-
expect(trigger).toHaveAttribute('aria-controls');
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
fireEvent.mouseLeave(trigger);
|
|
448
|
-
await waitFor(() => {
|
|
449
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
450
|
-
// The layer is gone from the DOM, so aria-controls is cleared.
|
|
451
|
-
expect(trigger).not.toHaveAttribute('aria-controls');
|
|
452
|
-
});
|
|
453
|
-
});
|
|
454
|
-
|
|
455
391
|
it('calls onOpenChange(true) when shown', async () => {
|
|
456
392
|
const onOpenChange = vi.fn();
|
|
457
393
|
render(
|
|
@@ -493,32 +429,16 @@ describe('HoverCard', () => {
|
|
|
493
429
|
|
|
494
430
|
it('supports text-only children with inline wrapper', () => {
|
|
495
431
|
render(
|
|
496
|
-
<HoverCard content={<span>Card content</span>}
|
|
432
|
+
<HoverCard content={<span>Card content</span>}>
|
|
497
433
|
Just text, no element
|
|
498
434
|
</HoverCard>,
|
|
499
435
|
);
|
|
500
436
|
// Text should be rendered
|
|
501
437
|
expect(screen.getByText('Just text, no element')).toBeInTheDocument();
|
|
502
|
-
//
|
|
503
|
-
const wrapper = screen.getByText('Just text, no element');
|
|
504
|
-
expect(wrapper.tagName).toBe('SPAN');
|
|
505
|
-
expect(wrapper).toHaveAttribute('aria-haspopup', 'dialog');
|
|
506
|
-
expect(wrapper).toHaveAttribute('aria-expanded', 'false');
|
|
507
|
-
// While closed, the layer is not in the DOM, so aria-controls is unset.
|
|
508
|
-
expect(wrapper).not.toHaveAttribute('aria-controls');
|
|
509
|
-
expect(wrapper).not.toHaveAttribute('aria-describedby');
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
it('keeps aria-describedby on text-only children when no label is provided', () => {
|
|
513
|
-
render(
|
|
514
|
-
<HoverCard content={<span>Card content</span>}>
|
|
515
|
-
Just text, no element
|
|
516
|
-
</HoverCard>,
|
|
517
|
-
);
|
|
438
|
+
// Should have aria-describedby on the wrapper span
|
|
518
439
|
const wrapper = screen.getByText('Just text, no element');
|
|
519
440
|
expect(wrapper.tagName).toBe('SPAN');
|
|
520
441
|
expect(wrapper).toHaveAttribute('aria-describedby');
|
|
521
|
-
expect(wrapper).not.toHaveAttribute('aria-haspopup');
|
|
522
442
|
});
|
|
523
443
|
|
|
524
444
|
describe('isDefaultOpen', () => {
|