@astronox/core 0.4.0 → 0.4.1
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/package.json +1 -1
- package/src/components/content/CardHeader/CardHeader.astro +20 -0
- package/src/components/content/CardHeader/CardHeader.css +21 -0
- package/src/components/feedback/Tooltip/tooltipController.ts +37 -5
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.astro +2 -0
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.css +12 -6
- package/src/composition/Grid/Grid.css +1 -1
- package/src/composition/SurfaceGroup/SurfaceGroup.css +18 -18
- package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +9 -0
- package/src/patterns/Combobox/comboboxController.ts +51 -12
- package/src/patterns/Dropdown/dropdownController.ts +17 -6
- package/src/patterns/Fullscreen/FullscreenControl.astro +3 -30
- package/src/patterns/Fullscreen/fullscreenController.ts +18 -22
- package/src/patterns/Table/Table.css +8 -2
- package/src/patterns/Table/TableFilterPanel.astro +84 -0
- package/src/patterns/Table/TableFilterPanel.css +139 -0
- package/src/patterns/Table/TableSkeleton.css +0 -3
- package/src/patterns/Table/tableFilterPanelController.ts +91 -0
- package/src/patterns/Table/tableProgressiveController.ts +78 -16
- package/src/patterns/Table/tableScrollPosition.ts +41 -0
- package/src/patterns/Table/tableSortController.ts +6 -3
- package/src/patterns/index.ts +2 -0
- package/src/styles/style.css +1 -0
package/package.json
CHANGED
|
@@ -17,6 +17,12 @@ type Props = HTMLAttributes<'header'> &
|
|
|
17
17
|
headingLevel?: HeadingLevel;
|
|
18
18
|
sticky?: boolean;
|
|
19
19
|
tone?: Tone;
|
|
20
|
+
trigger?: {
|
|
21
|
+
controls: string;
|
|
22
|
+
expanded: boolean;
|
|
23
|
+
hidden?: boolean;
|
|
24
|
+
label: string;
|
|
25
|
+
};
|
|
20
26
|
};
|
|
21
27
|
|
|
22
28
|
const {
|
|
@@ -29,6 +35,7 @@ const {
|
|
|
29
35
|
headingLevel = 2,
|
|
30
36
|
sticky = false,
|
|
31
37
|
tone,
|
|
38
|
+
trigger,
|
|
32
39
|
...attrs
|
|
33
40
|
}: Props = Astro.props;
|
|
34
41
|
const HeadingTag = `h${headingLevel}` as const;
|
|
@@ -80,6 +87,19 @@ const resolvedTone = tone ?? dataTone;
|
|
|
80
87
|
) : null
|
|
81
88
|
}
|
|
82
89
|
</Inline>
|
|
90
|
+
{
|
|
91
|
+
trigger ? (
|
|
92
|
+
<button
|
|
93
|
+
class="card-header__trigger"
|
|
94
|
+
type="button"
|
|
95
|
+
aria-controls={trigger.controls}
|
|
96
|
+
aria-expanded={trigger.expanded}
|
|
97
|
+
aria-label={trigger.label}
|
|
98
|
+
hidden={trigger.hidden}
|
|
99
|
+
data-card-header-trigger
|
|
100
|
+
/>
|
|
101
|
+
) : null
|
|
102
|
+
}
|
|
83
103
|
</div>
|
|
84
104
|
|
|
85
105
|
{
|
|
@@ -90,12 +90,33 @@
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
.card-header__main {
|
|
93
|
+
position: relative;
|
|
93
94
|
display: grid;
|
|
94
95
|
min-width: 0;
|
|
95
96
|
align-content: center;
|
|
96
97
|
padding: var(--nox-space-3) var(--nox-space-5);
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
.card-header__main:has(> .card-header__trigger) > [data-composition='inline'] {
|
|
101
|
+
pointer-events: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.card-header__trigger {
|
|
105
|
+
position: absolute;
|
|
106
|
+
z-index: 1;
|
|
107
|
+
inset: 0;
|
|
108
|
+
padding: 0;
|
|
109
|
+
border: 0;
|
|
110
|
+
background: transparent;
|
|
111
|
+
color: inherit;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.card-header__trigger:focus-visible {
|
|
116
|
+
outline: var(--nox-focus-ring-width) solid var(--nox-tone-focus-ring);
|
|
117
|
+
outline-offset: calc(var(--nox-focus-ring-width) * -1);
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
.card-header[data-has-end='true'] .card-header__main {
|
|
100
121
|
padding-block: var(--nox-space-1);
|
|
101
122
|
}
|
|
@@ -72,6 +72,7 @@ export function initTooltip(root: HTMLElement): () => void {
|
|
|
72
72
|
':scope > [data-tooltip-panel]',
|
|
73
73
|
);
|
|
74
74
|
if (!trigger || !panel) return noop;
|
|
75
|
+
const activeTrigger = trigger;
|
|
75
76
|
|
|
76
77
|
const hoverOnly = root.dataset.tooltipActivation === 'hover';
|
|
77
78
|
root.setAttribute(initializedAttribute, 'true');
|
|
@@ -106,6 +107,7 @@ export function initTooltip(root: HTMLElement): () => void {
|
|
|
106
107
|
let hoveredPanel = false;
|
|
107
108
|
let openListenersActive = false;
|
|
108
109
|
let pinned = false;
|
|
110
|
+
let lastPointerType = '';
|
|
109
111
|
|
|
110
112
|
const isOpen = (): boolean => panel.matches(':popover-open');
|
|
111
113
|
|
|
@@ -239,12 +241,12 @@ export function initTooltip(root: HTMLElement): () => void {
|
|
|
239
241
|
};
|
|
240
242
|
|
|
241
243
|
const handleTriggerPointerEnter = (event: PointerEvent): void => {
|
|
242
|
-
if (
|
|
244
|
+
if (event.pointerType === 'touch') return;
|
|
243
245
|
hoveredTrigger = true;
|
|
244
246
|
show();
|
|
245
247
|
};
|
|
246
248
|
const handleTriggerPointerLeave = (event: PointerEvent): void => {
|
|
247
|
-
if (
|
|
249
|
+
if (event.pointerType === 'touch') return;
|
|
248
250
|
hoveredTrigger = false;
|
|
249
251
|
|
|
250
252
|
if (hoverOnly) {
|
|
@@ -263,6 +265,7 @@ export function initTooltip(root: HTMLElement): () => void {
|
|
|
263
265
|
scheduleClose();
|
|
264
266
|
};
|
|
265
267
|
const handleFocusIn = (): void => {
|
|
268
|
+
if (lastPointerType === 'touch') return;
|
|
266
269
|
if (
|
|
267
270
|
hoverOnly &&
|
|
268
271
|
!trigger.matches(':focus-visible') &&
|
|
@@ -274,7 +277,23 @@ export function initTooltip(root: HTMLElement): () => void {
|
|
|
274
277
|
show();
|
|
275
278
|
};
|
|
276
279
|
const handleFocusOut = (): void => scheduleClose();
|
|
277
|
-
const
|
|
280
|
+
const handleTriggerPointerDown = (event: PointerEvent): void => {
|
|
281
|
+
lastPointerType = event.pointerType;
|
|
282
|
+
};
|
|
283
|
+
const handleTriggerKeyDown = (): void => {
|
|
284
|
+
lastPointerType = '';
|
|
285
|
+
};
|
|
286
|
+
const handleTriggerClick = (event: MouseEvent): void => {
|
|
287
|
+
const pointerType =
|
|
288
|
+
'PointerEvent' in window && event instanceof PointerEvent
|
|
289
|
+
? event.pointerType
|
|
290
|
+
: lastPointerType;
|
|
291
|
+
if (hoverOnly && pointerType !== 'touch') return;
|
|
292
|
+
if (pointerType === 'touch') {
|
|
293
|
+
event.preventDefault();
|
|
294
|
+
event.stopPropagation();
|
|
295
|
+
}
|
|
296
|
+
|
|
278
297
|
if (pinned) {
|
|
279
298
|
close();
|
|
280
299
|
return;
|
|
@@ -299,6 +318,12 @@ export function initTooltip(root: HTMLElement): () => void {
|
|
|
299
318
|
if (!(event instanceof CustomEvent) || event.detail?.root === root) return;
|
|
300
319
|
close();
|
|
301
320
|
};
|
|
321
|
+
const visibilityObserver =
|
|
322
|
+
'IntersectionObserver' in window
|
|
323
|
+
? new IntersectionObserver((entries) => {
|
|
324
|
+
if (entries.some((entry) => !entry.isIntersecting)) close();
|
|
325
|
+
})
|
|
326
|
+
: null;
|
|
302
327
|
function addOpenListeners(): void {
|
|
303
328
|
if (openListenersActive) return;
|
|
304
329
|
openListenersActive = true;
|
|
@@ -309,6 +334,7 @@ export function initTooltip(root: HTMLElement): () => void {
|
|
|
309
334
|
window.addEventListener('scroll', requestPosition, true);
|
|
310
335
|
window.visualViewport?.addEventListener('resize', requestPosition);
|
|
311
336
|
window.visualViewport?.addEventListener('scroll', requestPosition);
|
|
337
|
+
visibilityObserver?.observe(activeTrigger);
|
|
312
338
|
}
|
|
313
339
|
function removeOpenListeners(): void {
|
|
314
340
|
if (!openListenersActive) return;
|
|
@@ -320,13 +346,16 @@ export function initTooltip(root: HTMLElement): () => void {
|
|
|
320
346
|
window.removeEventListener('scroll', requestPosition, true);
|
|
321
347
|
window.visualViewport?.removeEventListener('resize', requestPosition);
|
|
322
348
|
window.visualViewport?.removeEventListener('scroll', requestPosition);
|
|
349
|
+
visibilityObserver?.unobserve(activeTrigger);
|
|
323
350
|
}
|
|
324
351
|
|
|
352
|
+
trigger.addEventListener('pointerdown', handleTriggerPointerDown);
|
|
325
353
|
trigger.addEventListener('pointerenter', handleTriggerPointerEnter);
|
|
326
354
|
trigger.addEventListener('pointerleave', handleTriggerPointerLeave);
|
|
327
355
|
trigger.addEventListener('focusin', handleFocusIn);
|
|
328
356
|
trigger.addEventListener('focusout', handleFocusOut);
|
|
329
|
-
|
|
357
|
+
trigger.addEventListener('keydown', handleTriggerKeyDown);
|
|
358
|
+
trigger.addEventListener('click', handleTriggerClick);
|
|
330
359
|
if (!hoverOnly) {
|
|
331
360
|
panel.addEventListener('pointerenter', handlePanelPointerEnter);
|
|
332
361
|
panel.addEventListener('pointerleave', handlePanelPointerLeave);
|
|
@@ -338,11 +367,14 @@ export function initTooltip(root: HTMLElement): () => void {
|
|
|
338
367
|
if (positionFrame !== undefined) {
|
|
339
368
|
window.cancelAnimationFrame(positionFrame);
|
|
340
369
|
}
|
|
370
|
+
visibilityObserver?.disconnect();
|
|
371
|
+
trigger.removeEventListener('pointerdown', handleTriggerPointerDown);
|
|
341
372
|
trigger.removeEventListener('pointerenter', handleTriggerPointerEnter);
|
|
342
373
|
trigger.removeEventListener('pointerleave', handleTriggerPointerLeave);
|
|
343
374
|
trigger.removeEventListener('focusin', handleFocusIn);
|
|
344
375
|
trigger.removeEventListener('focusout', handleFocusOut);
|
|
345
|
-
|
|
376
|
+
trigger.removeEventListener('keydown', handleTriggerKeyDown);
|
|
377
|
+
trigger.removeEventListener('click', handleTriggerClick);
|
|
346
378
|
if (!hoverOnly) {
|
|
347
379
|
panel.removeEventListener('pointerenter', handlePanelPointerEnter);
|
|
348
380
|
panel.removeEventListener('pointerleave', handlePanelPointerLeave);
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
'data-tone': dataTone,
|
|
17
17
|
items,
|
|
18
18
|
label = defaultCoreMessages.navigation.breadcrumbs,
|
|
19
|
+
tabindex: tabIndex = 0,
|
|
19
20
|
tone,
|
|
20
21
|
...attrs
|
|
21
22
|
} = Astro.props;
|
|
@@ -28,6 +29,7 @@ const resolvedTone = tone ?? dataTone ?? 'info';
|
|
|
28
29
|
{...attrs}
|
|
29
30
|
class:list={['breadcrumbs', className]}
|
|
30
31
|
aria-label={label}
|
|
32
|
+
tabindex={tabIndex}
|
|
31
33
|
data-widget="breadcrumbs"
|
|
32
34
|
data-tone={resolvedTone}
|
|
33
35
|
>
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
.breadcrumbs {
|
|
2
2
|
min-width: 0;
|
|
3
|
-
overflow:
|
|
3
|
+
overflow-x: auto;
|
|
4
|
+
overflow-y: hidden;
|
|
5
|
+
overscroll-behavior-inline: contain;
|
|
6
|
+
scrollbar-width: thin;
|
|
7
|
+
touch-action: pan-x pan-y;
|
|
4
8
|
color: var(--nox-tone-color);
|
|
5
9
|
font-size: var(--nox-font-size-sm);
|
|
6
10
|
font-weight: var(--nox-font-weight-medium);
|
|
7
11
|
line-height: 1.35;
|
|
8
12
|
}
|
|
9
13
|
|
|
14
|
+
.breadcrumbs:focus-visible {
|
|
15
|
+
outline: var(--nox-focus-ring-width) solid var(--nox-tone-focus-ring);
|
|
16
|
+
outline-offset: calc(var(--nox-focus-ring-width) * -1);
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
.breadcrumbs__list {
|
|
11
20
|
display: flex;
|
|
12
21
|
flex-wrap: nowrap;
|
|
13
22
|
align-items: stretch;
|
|
14
|
-
min-width:
|
|
23
|
+
min-width: max-content;
|
|
15
24
|
min-block-size: calc(
|
|
16
25
|
1.35em + var(--site-main-content-gap, var(--nox-space-5)) +
|
|
17
26
|
var(--site-main-content-gap, var(--nox-space-5))
|
|
@@ -30,8 +39,7 @@
|
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
.breadcrumbs__item:last-child {
|
|
33
|
-
flex:
|
|
34
|
-
overflow: hidden;
|
|
42
|
+
flex: 0 0 auto;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
.breadcrumbs__item > span {
|
|
@@ -63,8 +71,6 @@
|
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
.breadcrumbs__item:last-child > :is(a, span) {
|
|
66
|
-
overflow: hidden;
|
|
67
|
-
text-overflow: ellipsis;
|
|
68
74
|
white-space: nowrap;
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -17,24 +17,32 @@
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
.surface-group:has(> [data-sticky-surface])::before {
|
|
20
|
-
--surface-group-sticky-occlusion-size: calc(
|
|
21
|
-
var(--nox-layout-gap) + var(--nox-section-responsive-block-size)
|
|
22
|
-
);
|
|
23
|
-
|
|
24
20
|
position: sticky;
|
|
25
21
|
z-index: 39;
|
|
26
22
|
top: 0;
|
|
27
23
|
display: block;
|
|
28
24
|
min-width: 0;
|
|
29
|
-
min-height: var(
|
|
25
|
+
min-height: var(
|
|
26
|
+
--surface-group-sticky-occlusion-size,
|
|
27
|
+
calc(var(--nox-layout-gap) + var(--nox-section-responsive-block-size))
|
|
28
|
+
);
|
|
30
29
|
visibility: hidden;
|
|
31
|
-
flex: 0 0
|
|
32
|
-
|
|
30
|
+
flex: 0 0
|
|
31
|
+
var(
|
|
32
|
+
--surface-group-sticky-occlusion-size,
|
|
33
|
+
calc(var(--nox-layout-gap) + var(--nox-section-responsive-block-size))
|
|
34
|
+
);
|
|
35
|
+
margin-block-end: calc(
|
|
36
|
+
-1 *
|
|
37
|
+
var(
|
|
38
|
+
--surface-group-sticky-occlusion-size,
|
|
39
|
+
calc(var(--nox-layout-gap) + var(--nox-section-responsive-block-size))
|
|
40
|
+
)
|
|
41
|
+
);
|
|
33
42
|
background: var(--nox-page-background);
|
|
34
43
|
content: '';
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
.surface-group:has(> [data-sticky-surface]):not([data-sticky-measured])::before,
|
|
38
46
|
.surface-group[data-sticky-active]::before {
|
|
39
47
|
visibility: visible;
|
|
40
48
|
}
|
|
@@ -61,8 +69,8 @@
|
|
|
61
69
|
|
|
62
70
|
.surface-group
|
|
63
71
|
> [data-surface]:not([hidden]):has(~ [data-surface]:not([hidden])) {
|
|
64
|
-
border-end-start-radius: 0;
|
|
65
|
-
border-end-end-radius: 0;
|
|
72
|
+
border-end-start-radius: var(--surface-group-connected-border-end-radius, 0);
|
|
73
|
+
border-end-end-radius: var(--surface-group-connected-border-end-radius, 0);
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
.surface-group > [data-surface]:not([hidden]) ~ [data-surface]:not([hidden]) {
|
|
@@ -70,11 +78,3 @@
|
|
|
70
78
|
border-start-start-radius: 0;
|
|
71
79
|
border-start-end-radius: 0;
|
|
72
80
|
}
|
|
73
|
-
|
|
74
|
-
@container nox-surface-group (width < 32rem) {
|
|
75
|
-
.surface-group:has(> [data-sticky-surface])::before {
|
|
76
|
-
--surface-group-sticky-occlusion-size: calc(
|
|
77
|
-
var(--nox-layout-gap) + var(--nox-section-compact-block-size)
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -46,6 +46,10 @@ export const initStickySurfaceGroup = (root: HTMLElement): (() => void) => {
|
|
|
46
46
|
|
|
47
47
|
const stickyOffset =
|
|
48
48
|
Number.parseFloat(window.getComputedStyle(stickySurface).top) || 0;
|
|
49
|
+
root.style.setProperty(
|
|
50
|
+
'--surface-group-sticky-occlusion-size',
|
|
51
|
+
`${stickyOffset + stickySurface.getBoundingClientRect().height}px`,
|
|
52
|
+
);
|
|
49
53
|
observer = new IntersectionObserver(
|
|
50
54
|
([entry]) => {
|
|
51
55
|
if (!entry) return;
|
|
@@ -74,10 +78,14 @@ export const initStickySurfaceGroup = (root: HTMLElement): (() => void) => {
|
|
|
74
78
|
|
|
75
79
|
root.setAttribute('data-sticky-initialized', 'true');
|
|
76
80
|
window.addEventListener('resize', scheduleObserve, { passive: true });
|
|
81
|
+
const resizeObserver =
|
|
82
|
+
'ResizeObserver' in window ? new ResizeObserver(scheduleObserve) : null;
|
|
83
|
+
resizeObserver?.observe(stickySurface);
|
|
77
84
|
observe();
|
|
78
85
|
|
|
79
86
|
return () => {
|
|
80
87
|
window.removeEventListener('resize', scheduleObserve);
|
|
88
|
+
resizeObserver?.disconnect();
|
|
81
89
|
observer?.disconnect();
|
|
82
90
|
sentinel.remove();
|
|
83
91
|
|
|
@@ -88,5 +96,6 @@ export const initStickySurfaceGroup = (root: HTMLElement): (() => void) => {
|
|
|
88
96
|
root.removeAttribute('data-sticky-active');
|
|
89
97
|
root.removeAttribute('data-sticky-initialized');
|
|
90
98
|
root.removeAttribute('data-sticky-measured');
|
|
99
|
+
root.style.removeProperty('--surface-group-sticky-occlusion-size');
|
|
91
100
|
};
|
|
92
101
|
};
|
|
@@ -352,6 +352,7 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
352
352
|
let activeOption: HTMLButtonElement | null = null;
|
|
353
353
|
let positionFrame: number | null = null;
|
|
354
354
|
let viewportListenersAttached = false;
|
|
355
|
+
let openAtPointerDown: boolean | null = null;
|
|
355
356
|
|
|
356
357
|
const isOpen = (): boolean => root.hasAttribute('data-combobox-open');
|
|
357
358
|
|
|
@@ -391,6 +392,18 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
391
392
|
}
|
|
392
393
|
};
|
|
393
394
|
|
|
395
|
+
const scrollOptionIntoView = (option: HTMLElement | null): void => {
|
|
396
|
+
if (!option) return;
|
|
397
|
+
const optionsRect = optionsRoot.getBoundingClientRect();
|
|
398
|
+
const optionRect = option.getBoundingClientRect();
|
|
399
|
+
|
|
400
|
+
if (optionRect.top < optionsRect.top) {
|
|
401
|
+
optionsRoot.scrollTop -= optionsRect.top - optionRect.top;
|
|
402
|
+
} else if (optionRect.bottom > optionsRect.bottom) {
|
|
403
|
+
optionsRoot.scrollTop += optionRect.bottom - optionsRect.bottom;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
394
407
|
const positionPanel = (): void => {
|
|
395
408
|
if (!isOpen()) return;
|
|
396
409
|
|
|
@@ -490,7 +503,13 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
490
503
|
const target = event.target;
|
|
491
504
|
if (target instanceof Node && panel.contains(target)) return;
|
|
492
505
|
|
|
493
|
-
|
|
506
|
+
const siteViewport = document.querySelector<HTMLElement>(
|
|
507
|
+
'[data-site-viewport]',
|
|
508
|
+
);
|
|
509
|
+
const isSiteScroll = siteViewport
|
|
510
|
+
? target === siteViewport
|
|
511
|
+
: target === document || target === window;
|
|
512
|
+
if (isSiteScroll && window.matchMedia(compactViewportQuery).matches) {
|
|
494
513
|
close();
|
|
495
514
|
blurFocusedDescendant();
|
|
496
515
|
return;
|
|
@@ -547,6 +566,10 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
547
566
|
resetFilter();
|
|
548
567
|
if (panel.matches(':popover-open')) panel.hidePopover();
|
|
549
568
|
panel.hidden = true;
|
|
569
|
+
panel.style.removeProperty('--combobox-panel-inline-start');
|
|
570
|
+
panel.style.removeProperty('--combobox-panel-block-start');
|
|
571
|
+
panel.style.removeProperty('--combobox-panel-inline-size');
|
|
572
|
+
panel.style.removeProperty('--combobox-panel-available-block-size');
|
|
550
573
|
|
|
551
574
|
if (restoreFocus) owner.focus();
|
|
552
575
|
};
|
|
@@ -573,7 +596,7 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
573
596
|
(option) => option.getAttribute('aria-selected') === 'true',
|
|
574
597
|
) ?? null;
|
|
575
598
|
setActiveOption(null);
|
|
576
|
-
selectedOption
|
|
599
|
+
scrollOptionIntoView(selectedOption);
|
|
577
600
|
|
|
578
601
|
if (focusSearch && search) {
|
|
579
602
|
window.requestAnimationFrame(() => {
|
|
@@ -601,7 +624,7 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
601
624
|
visibleOptions.length;
|
|
602
625
|
const nextOption = visibleOptions[nextIndex] ?? null;
|
|
603
626
|
setActiveOption(nextOption);
|
|
604
|
-
nextOption
|
|
627
|
+
scrollOptionIntoView(nextOption);
|
|
605
628
|
};
|
|
606
629
|
|
|
607
630
|
const selectOption = (
|
|
@@ -687,13 +710,23 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
687
710
|
};
|
|
688
711
|
|
|
689
712
|
const toggleControl = (restoreFocus = true): void => {
|
|
690
|
-
|
|
713
|
+
const wasOpen = openAtPointerDown ?? isOpen();
|
|
714
|
+
openAtPointerDown = null;
|
|
715
|
+
if (wasOpen) {
|
|
691
716
|
close(restoreFocus);
|
|
692
717
|
} else {
|
|
693
718
|
open();
|
|
694
719
|
}
|
|
695
720
|
};
|
|
696
721
|
|
|
722
|
+
const handleOwnerPointerDown = (): void => {
|
|
723
|
+
openAtPointerDown = isOpen();
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const handleOwnerPointerCancel = (): void => {
|
|
727
|
+
openAtPointerDown = null;
|
|
728
|
+
};
|
|
729
|
+
|
|
697
730
|
const handleOptionsTriggerClick = (): void => {
|
|
698
731
|
toggleControl(false);
|
|
699
732
|
};
|
|
@@ -703,6 +736,7 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
703
736
|
};
|
|
704
737
|
|
|
705
738
|
const handleOwnerKeydown = (event: KeyboardEvent): void => {
|
|
739
|
+
openAtPointerDown = null;
|
|
706
740
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
707
741
|
event.preventDefault();
|
|
708
742
|
if (!isOpen()) {
|
|
@@ -799,12 +833,9 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
799
833
|
const handleNumberInputClick = (): void => {
|
|
800
834
|
if (!numberInput) return;
|
|
801
835
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
open(false);
|
|
806
|
-
numberInput.select();
|
|
807
|
-
}
|
|
836
|
+
const wasOpen = openAtPointerDown ?? isOpen();
|
|
837
|
+
toggleControl(false);
|
|
838
|
+
if (!wasOpen) numberInput.select();
|
|
808
839
|
};
|
|
809
840
|
|
|
810
841
|
const handleNumberInput = (): void => {
|
|
@@ -825,7 +856,7 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
825
856
|
});
|
|
826
857
|
}
|
|
827
858
|
setActiveOption(matchingOption ?? null);
|
|
828
|
-
matchingOption
|
|
859
|
+
scrollOptionIntoView(matchingOption ?? null);
|
|
829
860
|
};
|
|
830
861
|
|
|
831
862
|
const handleNumberInputChange = (): void => {
|
|
@@ -851,7 +882,7 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
851
882
|
(option) => option.getAttribute('aria-selected') === 'true',
|
|
852
883
|
) ?? null;
|
|
853
884
|
setActiveOption(selectedOption);
|
|
854
|
-
selectedOption
|
|
885
|
+
scrollOptionIntoView(selectedOption);
|
|
855
886
|
}
|
|
856
887
|
};
|
|
857
888
|
|
|
@@ -877,6 +908,8 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
877
908
|
|
|
878
909
|
if (mode === 'options' && trigger && search) {
|
|
879
910
|
control.addEventListener('click', handleOptionsControlClick);
|
|
911
|
+
trigger.addEventListener('pointerdown', handleOwnerPointerDown);
|
|
912
|
+
trigger.addEventListener('pointercancel', handleOwnerPointerCancel);
|
|
880
913
|
trigger.addEventListener('click', handleOptionsTriggerClick);
|
|
881
914
|
trigger.addEventListener('keydown', handleOwnerKeydown);
|
|
882
915
|
search.addEventListener('input', filterOptions);
|
|
@@ -884,6 +917,8 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
884
917
|
}
|
|
885
918
|
|
|
886
919
|
if (mode === 'number' && numberInput) {
|
|
920
|
+
numberInput.addEventListener('pointerdown', handleOwnerPointerDown);
|
|
921
|
+
numberInput.addEventListener('pointercancel', handleOwnerPointerCancel);
|
|
887
922
|
numberInput.addEventListener('click', handleNumberInputClick);
|
|
888
923
|
numberInput.addEventListener('input', handleNumberInput);
|
|
889
924
|
numberInput.addEventListener('change', handleNumberInputChange);
|
|
@@ -908,10 +943,14 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
908
943
|
detachViewportListeners();
|
|
909
944
|
controlResizeObserver?.disconnect();
|
|
910
945
|
control.removeEventListener('click', handleOptionsControlClick);
|
|
946
|
+
trigger?.removeEventListener('pointerdown', handleOwnerPointerDown);
|
|
947
|
+
trigger?.removeEventListener('pointercancel', handleOwnerPointerCancel);
|
|
911
948
|
trigger?.removeEventListener('click', handleOptionsTriggerClick);
|
|
912
949
|
trigger?.removeEventListener('keydown', handleOwnerKeydown);
|
|
913
950
|
search?.removeEventListener('input', filterOptions);
|
|
914
951
|
search?.removeEventListener('keydown', handleSearchKeydown);
|
|
952
|
+
numberInput?.removeEventListener('pointerdown', handleOwnerPointerDown);
|
|
953
|
+
numberInput?.removeEventListener('pointercancel', handleOwnerPointerCancel);
|
|
915
954
|
numberInput?.removeEventListener('click', handleNumberInputClick);
|
|
916
955
|
numberInput?.removeEventListener('input', handleNumberInput);
|
|
917
956
|
numberInput?.removeEventListener('change', handleNumberInputChange);
|
|
@@ -25,6 +25,7 @@ export function initDropdown(root: HTMLElement): () => void {
|
|
|
25
25
|
const triggerMode = getTriggerMode(root);
|
|
26
26
|
const hoverMedia = window.matchMedia('(hover: hover)');
|
|
27
27
|
let isOpen = !content.hidden;
|
|
28
|
+
let openAtPointerDown: boolean | null = null;
|
|
28
29
|
|
|
29
30
|
const canOpenOnHover = (): boolean =>
|
|
30
31
|
triggerMode === 'hover' && hoverMedia.matches;
|
|
@@ -61,12 +62,17 @@ export function initDropdown(root: HTMLElement): () => void {
|
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
const handleTriggerClick = (): void => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
const wasOpen = openAtPointerDown ?? isOpen;
|
|
66
|
+
openAtPointerDown = null;
|
|
67
|
+
setOpen(!wasOpen);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleTriggerPointerDown = (): void => {
|
|
71
|
+
openAtPointerDown = isOpen;
|
|
72
|
+
};
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
const handleTriggerPointerCancel = (): void => {
|
|
75
|
+
openAtPointerDown = null;
|
|
70
76
|
};
|
|
71
77
|
|
|
72
78
|
const handleRootPointerEnter = (): void => {
|
|
@@ -88,12 +94,13 @@ export function initDropdown(root: HTMLElement): () => void {
|
|
|
88
94
|
};
|
|
89
95
|
|
|
90
96
|
const handleTriggerFocus = (): void => {
|
|
91
|
-
if (
|
|
97
|
+
if (canOpenOnHover()) {
|
|
92
98
|
setOpen(true);
|
|
93
99
|
}
|
|
94
100
|
};
|
|
95
101
|
|
|
96
102
|
const handleTriggerKeydown = (event: KeyboardEvent): void => {
|
|
103
|
+
openAtPointerDown = null;
|
|
97
104
|
if (event.key === 'ArrowDown') {
|
|
98
105
|
event.preventDefault();
|
|
99
106
|
openAndFocusFirstItem();
|
|
@@ -131,6 +138,8 @@ export function initDropdown(root: HTMLElement): () => void {
|
|
|
131
138
|
|
|
132
139
|
setOpen(isOpen);
|
|
133
140
|
|
|
141
|
+
trigger.addEventListener('pointerdown', handleTriggerPointerDown);
|
|
142
|
+
trigger.addEventListener('pointercancel', handleTriggerPointerCancel);
|
|
134
143
|
trigger.addEventListener('click', handleTriggerClick);
|
|
135
144
|
trigger.addEventListener('focus', handleTriggerFocus);
|
|
136
145
|
trigger.addEventListener('keydown', handleTriggerKeydown);
|
|
@@ -143,6 +152,8 @@ export function initDropdown(root: HTMLElement): () => void {
|
|
|
143
152
|
|
|
144
153
|
return () => {
|
|
145
154
|
close();
|
|
155
|
+
trigger.removeEventListener('pointerdown', handleTriggerPointerDown);
|
|
156
|
+
trigger.removeEventListener('pointercancel', handleTriggerPointerCancel);
|
|
146
157
|
trigger.removeEventListener('click', handleTriggerClick);
|
|
147
158
|
trigger.removeEventListener('focus', handleTriggerFocus);
|
|
148
159
|
trigger.removeEventListener('keydown', handleTriggerKeydown);
|
|
@@ -4,13 +4,6 @@ import type { Tone } from '../../theme/contracts';
|
|
|
4
4
|
import Button from '../../components/actions/Button/Button.astro';
|
|
5
5
|
import EnterFullscreenIcon from '../../components/icons/EnterFullscreenIcon.astro';
|
|
6
6
|
import ExitFullscreenIcon from '../../components/icons/ExitFullscreenIcon.astro';
|
|
7
|
-
import ExternalLinkIcon from '../../components/icons/ExternalLinkIcon.astro';
|
|
8
|
-
|
|
9
|
-
type FullscreenUnsupportedFallback = {
|
|
10
|
-
href: string;
|
|
11
|
-
label: string;
|
|
12
|
-
target?: '_blank' | '_self';
|
|
13
|
-
};
|
|
14
7
|
|
|
15
8
|
type Props = Omit<
|
|
16
9
|
HTMLAttributes<'button'>,
|
|
@@ -21,7 +14,7 @@ type Props = Omit<
|
|
|
21
14
|
exitLabel: string;
|
|
22
15
|
targetId: string;
|
|
23
16
|
tone?: Tone;
|
|
24
|
-
|
|
17
|
+
unavailableLabel?: string;
|
|
25
18
|
};
|
|
26
19
|
|
|
27
20
|
const {
|
|
@@ -30,7 +23,7 @@ const {
|
|
|
30
23
|
exitLabel,
|
|
31
24
|
targetId,
|
|
32
25
|
tone,
|
|
33
|
-
|
|
26
|
+
unavailableLabel = enterLabel,
|
|
34
27
|
...attrs
|
|
35
28
|
}: Props = Astro.props;
|
|
36
29
|
const resolvedClass = ['fullscreen-control', className]
|
|
@@ -54,6 +47,7 @@ const resolvedClass = ['fullscreen-control', className]
|
|
|
54
47
|
data-fullscreen-enter-label={enterLabel}
|
|
55
48
|
data-fullscreen-exit-label={exitLabel}
|
|
56
49
|
data-fullscreen-target={targetId}
|
|
50
|
+
data-fullscreen-unavailable-label={unavailableLabel}
|
|
57
51
|
>
|
|
58
52
|
<span class="fullscreen-control__icon" data-fullscreen-enter-icon>
|
|
59
53
|
<EnterFullscreenIcon />
|
|
@@ -63,27 +57,6 @@ const resolvedClass = ['fullscreen-control', className]
|
|
|
63
57
|
</span>
|
|
64
58
|
</Button>
|
|
65
59
|
|
|
66
|
-
{
|
|
67
|
-
unsupportedFallback ? (
|
|
68
|
-
<Button
|
|
69
|
-
href={unsupportedFallback.href}
|
|
70
|
-
target={unsupportedFallback.target ?? '_blank'}
|
|
71
|
-
borderless
|
|
72
|
-
full
|
|
73
|
-
iconOnly
|
|
74
|
-
tone={tone}
|
|
75
|
-
aria-label={unsupportedFallback.label}
|
|
76
|
-
title={unsupportedFallback.label}
|
|
77
|
-
hidden
|
|
78
|
-
data-fullscreen-unsupported-fallback
|
|
79
|
-
>
|
|
80
|
-
<span class="fullscreen-control__icon">
|
|
81
|
-
<ExternalLinkIcon />
|
|
82
|
-
</span>
|
|
83
|
-
</Button>
|
|
84
|
-
) : null
|
|
85
|
-
}
|
|
86
|
-
|
|
87
60
|
<script>
|
|
88
61
|
import { initFullscreenControl } from './fullscreenController';
|
|
89
62
|
|