@opalui/react 0.0.0-stage → 0.0.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/dist/components/button/button.css +157 -0
- package/dist/components/button/button.css.js +1 -0
- package/dist/components/button/button.d.ts +26 -0
- package/dist/components/button/button.js +39 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/glass/glass.css +16 -0
- package/dist/components/glass/glass.css.js +1 -0
- package/dist/components/glass/glass.d.ts +5 -0
- package/dist/components/glass/glass.js +19 -0
- package/dist/components/glass/glass.js.map +1 -0
- package/dist/components/input/input.css +43 -0
- package/dist/components/input/input.css.js +1 -0
- package/dist/components/input/input.d.ts +17 -0
- package/dist/components/input/input.js +36 -0
- package/dist/components/input/input.js.map +1 -0
- package/dist/components/menu/menu.css +85 -0
- package/dist/components/menu/menu.css.js +1 -0
- package/dist/components/menu/menu.d.ts +18 -0
- package/dist/components/menu/menu.js +125 -0
- package/dist/components/menu/menu.js.map +1 -0
- package/dist/components/segmented-control/segmented-control.css +109 -0
- package/dist/components/segmented-control/segmented-control.css.js +1 -0
- package/dist/components/segmented-control/segmented-control.d.ts +21 -0
- package/dist/components/segmented-control/segmented-control.js +147 -0
- package/dist/components/segmented-control/segmented-control.js.map +1 -0
- package/dist/components/select/select.css +137 -0
- package/dist/components/select/select.css.js +1 -0
- package/dist/components/select/select.d.ts +27 -0
- package/dist/components/select/select.js +140 -0
- package/dist/components/select/select.js.map +1 -0
- package/dist/components/slider/slider.css +87 -0
- package/dist/components/slider/slider.css.js +1 -0
- package/dist/components/slider/slider.d.ts +17 -0
- package/dist/components/slider/slider.js +40 -0
- package/dist/components/slider/slider.js.map +1 -0
- package/dist/components/switch/switch.css +103 -0
- package/dist/components/switch/switch.css.js +1 -0
- package/dist/components/switch/switch.d.ts +14 -0
- package/dist/components/switch/switch.js +74 -0
- package/dist/components/switch/switch.js.map +1 -0
- package/dist/components/textarea/textarea.css +29 -0
- package/dist/components/textarea/textarea.css.js +1 -0
- package/dist/components/textarea/textarea.d.ts +8 -0
- package/dist/components/textarea/textarea.js +17 -0
- package/dist/components/textarea/textarea.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/glass/lens.d.ts +44 -0
- package/dist/internal/glass/lens.js +116 -0
- package/dist/internal/glass/lens.js.map +1 -0
- package/dist/internal/glass/map.d.ts +1 -0
- package/dist/internal/glass/map.js +48 -0
- package/dist/internal/glass/map.js.map +1 -0
- package/dist/internal/glass/material.d.ts +48 -0
- package/dist/internal/glass/material.js +22 -0
- package/dist/internal/glass/material.js.map +1 -0
- package/dist/internal/glass/styles.d.ts +2 -0
- package/dist/internal/glass/styles.js +4 -0
- package/dist/internal/glass/styles.js.map +1 -0
- package/dist/internal/glass/support.d.ts +5 -0
- package/dist/internal/glass/support.js +8 -0
- package/dist/internal/glass/support.js.map +1 -0
- package/dist/internal/glass.css +7 -0
- package/dist/internal/glass.css.js +1 -0
- package/dist/internal/styles.d.ts +2 -0
- package/dist/internal/styles.js +9 -0
- package/dist/internal/styles.js.map +1 -0
- package/dist/styles.css +11 -0
- package/dist/styles.css.js +1 -0
- package/package.json +90 -4
- package/README.md +0 -3
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
4
|
+
import { Button } from '../button/button.js';
|
|
5
|
+
import { stylesheetHref } from '../../internal/styles.js';
|
|
6
|
+
import componentCss from './menu.css.js';
|
|
7
|
+
import glassCss from '../../internal/glass/styles.js';
|
|
8
|
+
const css = `${glassCss}\n${componentCss}`;
|
|
9
|
+
const styleHref = stylesheetHref('menu', css);
|
|
10
|
+
/** A compact action menu. Styles are included automatically. */
|
|
11
|
+
export function Menu({ items, icon, children, disabled, className, 'aria-label': label }) {
|
|
12
|
+
const id = useId();
|
|
13
|
+
const trigger = useRef(null);
|
|
14
|
+
const panel = useRef(null);
|
|
15
|
+
const [open, setOpen] = useState(false);
|
|
16
|
+
const [highlight, setHighlight] = useState(null);
|
|
17
|
+
function highlightItem(item) {
|
|
18
|
+
setHighlight({ top: item.offsetTop, height: item.offsetHeight });
|
|
19
|
+
}
|
|
20
|
+
const search = useRef({ text: '', time: 0 });
|
|
21
|
+
const options = () => Array.from(panel.current?.querySelectorAll('[role="menuitem"]:not(:disabled)') ?? []);
|
|
22
|
+
function close(restoreFocus = true) {
|
|
23
|
+
panel.current?.hidePopover?.();
|
|
24
|
+
setOpen(false);
|
|
25
|
+
setHighlight(null);
|
|
26
|
+
if (restoreFocus)
|
|
27
|
+
trigger.current?.focus({ preventScroll: true });
|
|
28
|
+
}
|
|
29
|
+
function position() {
|
|
30
|
+
if (!trigger.current || !panel.current)
|
|
31
|
+
return;
|
|
32
|
+
const anchor = trigger.current.getBoundingClientRect();
|
|
33
|
+
const menu = panel.current;
|
|
34
|
+
const width = menu.offsetWidth;
|
|
35
|
+
const height = menu.offsetHeight;
|
|
36
|
+
menu.style.left = `${Math.max(8, Math.min(anchor.left, window.innerWidth - width - 8))}px`;
|
|
37
|
+
menu.style.top = `${Math.max(8, Math.min(anchor.top, window.innerHeight - height - 8))}px`;
|
|
38
|
+
menu.style.transformOrigin = `${Math.max(0, anchor.left - parseFloat(menu.style.left) + anchor.width / 2)}px ${Math.max(0, anchor.top - parseFloat(menu.style.top) + anchor.height / 2)}px`;
|
|
39
|
+
}
|
|
40
|
+
function show(last = false) {
|
|
41
|
+
if (disabled)
|
|
42
|
+
return;
|
|
43
|
+
const menu = panel.current;
|
|
44
|
+
if (!menu)
|
|
45
|
+
return;
|
|
46
|
+
menu.dataset.open = 'true';
|
|
47
|
+
menu.showPopover?.();
|
|
48
|
+
position();
|
|
49
|
+
setOpen(true);
|
|
50
|
+
search.current = { text: '', time: 0 };
|
|
51
|
+
const enabled = options();
|
|
52
|
+
(last ? enabled.at(-1) : enabled[0])?.focus({ preventScroll: true });
|
|
53
|
+
if (!enabled.length)
|
|
54
|
+
menu.focus({ preventScroll: true });
|
|
55
|
+
}
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!open)
|
|
58
|
+
return;
|
|
59
|
+
const outside = (event) => {
|
|
60
|
+
if (!panel.current?.contains(event.target) && !trigger.current?.contains(event.target))
|
|
61
|
+
close(false);
|
|
62
|
+
};
|
|
63
|
+
const blur = () => close(false);
|
|
64
|
+
document.addEventListener('pointerdown', outside);
|
|
65
|
+
window.addEventListener('resize', position);
|
|
66
|
+
window.addEventListener('scroll', position, true);
|
|
67
|
+
window.addEventListener('blur', blur);
|
|
68
|
+
return () => {
|
|
69
|
+
document.removeEventListener('pointerdown', outside);
|
|
70
|
+
window.removeEventListener('resize', position);
|
|
71
|
+
window.removeEventListener('scroll', position, true);
|
|
72
|
+
window.removeEventListener('blur', blur);
|
|
73
|
+
};
|
|
74
|
+
}, [open]);
|
|
75
|
+
useEffect(() => { if (disabled)
|
|
76
|
+
close(false); }, [disabled]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!open || !panel.current)
|
|
79
|
+
return;
|
|
80
|
+
const observer = new ResizeObserver(() => {
|
|
81
|
+
const active = document.activeElement;
|
|
82
|
+
if (active instanceof HTMLButtonElement && panel.current?.contains(active))
|
|
83
|
+
highlightItem(active);
|
|
84
|
+
});
|
|
85
|
+
observer.observe(panel.current);
|
|
86
|
+
options().forEach(item => observer.observe(item));
|
|
87
|
+
return () => observer.disconnect();
|
|
88
|
+
}, [open, items]);
|
|
89
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { href: styleHref, precedence: "opalui", children: css }), _jsxs("span", { className: ['opal-menu', className].filter(Boolean).join(' '), children: [_jsx(Button, { ref: trigger, "aria-label": label, "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": id, disabled: disabled, icon: icon ?? (children ? undefined : _jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", children: [_jsx("circle", { cx: "5", cy: "12", r: "1.6" }), _jsx("circle", { cx: "12", cy: "12", r: "1.6" }), _jsx("circle", { cx: "19", cy: "12", r: "1.6" })] })), onClick: () => open ? close() : show(), onKeyDown: event => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
show(event.key === 'ArrowUp');
|
|
92
|
+
} }, children: children }), _jsxs("div", { ref: panel, id: id, role: "menu", "aria-label": label, tabIndex: -1, popover: "manual", className: "opal-menu-panel", "data-open": open || undefined, onKeyDown: event => {
|
|
93
|
+
const enabled = options();
|
|
94
|
+
const current = enabled.indexOf(document.activeElement);
|
|
95
|
+
if (event.key === 'Escape') {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
event.stopPropagation();
|
|
98
|
+
close();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (event.key === 'Tab') {
|
|
102
|
+
close();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
const index = event.key === 'Home' ? 0 : event.key === 'End' ? enabled.length - 1
|
|
108
|
+
: (current + (event.key === 'ArrowDown' ? 1 : -1) + enabled.length) % enabled.length;
|
|
109
|
+
enabled[index]?.focus();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (event.key.length === 1 && event.key !== ' ' && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
search.current.text = (now - search.current.time < 600 ? search.current.text : '') + event.key.toLowerCase();
|
|
116
|
+
search.current.time = now;
|
|
117
|
+
const query = new Set(search.current.text).size === 1 ? event.key.toLowerCase() : search.current.text;
|
|
118
|
+
const ordered = [...enabled.slice(current + 1), ...enabled.slice(0, current + 1)];
|
|
119
|
+
ordered.find(item => item.textContent?.trim().toLowerCase().startsWith(query))?.focus();
|
|
120
|
+
}
|
|
121
|
+
}, children: [highlight && _jsx("span", { className: "opal-menu-highlight", "aria-hidden": "true", style: { transform: `translateY(${highlight.top}px)`, height: highlight.height } }), items.map((item, index) => item === 'separator' ? _jsx("div", { role: "separator", className: "opal-menu-separator" }, index)
|
|
122
|
+
: _jsxs("button", { type: "button", role: "menuitem", tabIndex: -1, disabled: item.disabled, className: "opal-menu-item", "data-destructive": item.destructive || undefined, onFocus: event => highlightItem(event.currentTarget), onPointerMove: event => { if (event.pointerType === 'mouse' && !item.disabled && document.activeElement !== event.currentTarget)
|
|
123
|
+
event.currentTarget.focus({ preventScroll: true }); }, onClick: () => { close(); item.onSelect?.(); }, children: [item.icon && _jsx("span", { className: "opal-menu-icon", "aria-hidden": "true", children: item.icon }), _jsx("span", { children: item.label })] }, index))] })] })] });
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu.js","sourceRoot":"","sources":["../../../src/components/menu/menu.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { useEffect, useId, useRef, useState, type ReactNode } from 'react'\nimport { Button } from '../button/button.js'\nimport { stylesheetHref } from '../../internal/styles.js'\nimport componentCss from './menu.css?inline'\nimport glassCss from '../../internal/glass/styles.js'\nconst css = `${glassCss}\\n${componentCss}`\n\nconst styleHref = stylesheetHref('menu', css)\nexport type MenuItem = { label: string; icon?: ReactNode; onSelect?: () => void; disabled?: boolean; destructive?: boolean } | 'separator'\nexport type MenuProps = {\n 'aria-label': string\n items: readonly MenuItem[]\n icon?: ReactNode\n children?: ReactNode\n disabled?: boolean\n className?: string\n}\n\n/** A compact action menu. Styles are included automatically. */\nexport function Menu({ items, icon, children, disabled, className, 'aria-label': label }: MenuProps) {\n const id = useId()\n const trigger = useRef<HTMLButtonElement>(null)\n const panel = useRef<HTMLDivElement>(null)\n const [open, setOpen] = useState(false)\n const [highlight, setHighlight] = useState<{ top: number; height: number } | null>(null)\n function highlightItem(item: HTMLButtonElement) {\n setHighlight({ top: item.offsetTop, height: item.offsetHeight })\n }\n const search = useRef({ text: '', time: 0 })\n const options = () => Array.from(panel.current?.querySelectorAll<HTMLButtonElement>('[role=\"menuitem\"]:not(:disabled)') ?? [])\n function close(restoreFocus = true) {\n panel.current?.hidePopover?.()\n setOpen(false)\n setHighlight(null)\n if (restoreFocus) trigger.current?.focus({ preventScroll: true })\n }\n function position() {\n if (!trigger.current || !panel.current) return\n const anchor = trigger.current.getBoundingClientRect()\n const menu = panel.current\n const width = menu.offsetWidth\n const height = menu.offsetHeight\n menu.style.left = `${Math.max(8, Math.min(anchor.left, window.innerWidth - width - 8))}px`\n menu.style.top = `${Math.max(8, Math.min(anchor.top, window.innerHeight - height - 8))}px`\n menu.style.transformOrigin = `${Math.max(0, anchor.left - parseFloat(menu.style.left) + anchor.width / 2)}px ${Math.max(0, anchor.top - parseFloat(menu.style.top) + anchor.height / 2)}px`\n }\n function show(last = false) {\n if (disabled) return\n const menu = panel.current\n if (!menu) return\n menu.dataset.open = 'true'\n menu.showPopover?.()\n position()\n setOpen(true)\n search.current = { text: '', time: 0 }\n const enabled = options()\n ;(last ? enabled.at(-1) : enabled[0])?.focus({ preventScroll: true })\n if (!enabled.length) menu.focus({ preventScroll: true })\n }\n useEffect(() => {\n if (!open) return\n const outside = (event: PointerEvent) => {\n if (!panel.current?.contains(event.target as Node) && !trigger.current?.contains(event.target as Node)) close(false)\n }\n const blur = () => close(false)\n document.addEventListener('pointerdown', outside)\n window.addEventListener('resize', position)\n window.addEventListener('scroll', position, true)\n window.addEventListener('blur', blur)\n return () => {\n document.removeEventListener('pointerdown', outside)\n window.removeEventListener('resize', position)\n window.removeEventListener('scroll', position, true)\n window.removeEventListener('blur', blur)\n }\n }, [open])\n useEffect(() => { if (disabled) close(false) }, [disabled])\n useEffect(() => {\n if (!open || !panel.current) return\n const observer = new ResizeObserver(() => {\n const active = document.activeElement\n if (active instanceof HTMLButtonElement && panel.current?.contains(active)) highlightItem(active)\n })\n observer.observe(panel.current)\n options().forEach(item => observer.observe(item))\n return () => observer.disconnect()\n }, [open, items])\n return <>\n <style href={styleHref} precedence=\"opalui\">{css}</style>\n <span className={['opal-menu', className].filter(Boolean).join(' ')}>\n <Button ref={trigger} aria-label={label} aria-haspopup=\"menu\" aria-expanded={open} aria-controls={id}\n disabled={disabled} icon={icon ?? (children ? undefined : <svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"5\" cy=\"12\" r=\"1.6\" /><circle cx=\"12\" cy=\"12\" r=\"1.6\" /><circle cx=\"19\" cy=\"12\" r=\"1.6\" /></svg>)}\n onClick={() => open ? close() : show()}\n onKeyDown={event => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); show(event.key === 'ArrowUp') } }}>\n {children}\n </Button>\n <div ref={panel} id={id} role=\"menu\" aria-label={label} tabIndex={-1} popover=\"manual\"\n className=\"opal-menu-panel\" data-open={open || undefined}\n onKeyDown={event => {\n const enabled = options()\n const current = enabled.indexOf(document.activeElement as HTMLButtonElement)\n if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); close(); return }\n if (event.key === 'Tab') { close(); return }\n if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {\n event.preventDefault()\n const index = event.key === 'Home' ? 0 : event.key === 'End' ? enabled.length - 1\n : (current + (event.key === 'ArrowDown' ? 1 : -1) + enabled.length) % enabled.length\n enabled[index]?.focus(); return\n }\n if (event.key.length === 1 && event.key !== ' ' && !event.metaKey && !event.ctrlKey && !event.altKey) {\n event.preventDefault()\n const now = Date.now()\n search.current.text = (now - search.current.time < 600 ? search.current.text : '') + event.key.toLowerCase()\n search.current.time = now\n const query = new Set(search.current.text).size === 1 ? event.key.toLowerCase() : search.current.text\n const ordered = [...enabled.slice(current + 1), ...enabled.slice(0, current + 1)]\n ordered.find(item => item.textContent?.trim().toLowerCase().startsWith(query))?.focus()\n }\n }}>\n {highlight && <span className=\"opal-menu-highlight\" aria-hidden=\"true\"\n style={{ transform: `translateY(${highlight.top}px)`, height: highlight.height }} />}\n {items.map((item, index) => item === 'separator' ? <div key={index} role=\"separator\" className=\"opal-menu-separator\" />\n : <button key={index} type=\"button\" role=\"menuitem\" tabIndex={-1} disabled={item.disabled}\n className=\"opal-menu-item\" data-destructive={item.destructive || undefined}\n onFocus={event => highlightItem(event.currentTarget)}\n onPointerMove={event => { if (event.pointerType === 'mouse' && !item.disabled && document.activeElement !== event.currentTarget) event.currentTarget.focus({ preventScroll: true }) }}\n onClick={() => { close(); item.onSelect?.() }}>\n {item.icon && <span className=\"opal-menu-icon\" aria-hidden=\"true\">{item.icon}</span>}\n <span>{item.label}</span>\n </button>)}\n </div>\n </span>\n </>\n}\n"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* Shared material. Public variables can be set once on a theme container. */
|
|
2
|
+
:where(.opal-glass, .opal-button, .opal-input, .opal-menu-panel, .opal-switch, .opal-slider, .opal-segmented, .opal-textarea, .opal-select) {
|
|
3
|
+
--_opal-glass-rim: var(--opal-glass-rim, inset 0 0 0 .5px rgb(255 255 255 / 7%), inset .65px 1px .65px rgb(255 255 255 / var(--opal-material-rim, .28)), inset -.4px -.65px .8px rgb(255 255 255 / calc(var(--opal-material-rim, .28) * .43)));
|
|
4
|
+
--_opal-glass-shadow: var(--opal-glass-shadow, 0 3px 8px rgb(15 34 47 / 12%));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
:where(.opal-input,.opal-textarea,.opal-select) { --opal-material-frost:12px; --opal-material-rim:0.22; }
|
|
8
|
+
:where(.opal-menu-panel) { --opal-material-frost:24px; --opal-material-rim:0.3; }
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
.opal-segmented-filter { position: absolute; pointer-events: none; }
|
|
12
|
+
.opal-segmented {
|
|
13
|
+
position: relative;
|
|
14
|
+
display: inline-grid;
|
|
15
|
+
grid-template-columns: repeat(var(--opal-segment-count), minmax(0, 1fr));
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
padding: 3px;
|
|
18
|
+
border-radius: 999px;
|
|
19
|
+
background: var(--opal-segmented-background, rgb(0 0 0 / 5%));
|
|
20
|
+
color: var(--opal-segmented-color, #202020);
|
|
21
|
+
box-shadow: var(--opal-segmented-shadow, var(--_opal-glass-shadow));
|
|
22
|
+
vertical-align: middle;
|
|
23
|
+
max-width: 100%;
|
|
24
|
+
isolation: isolate;
|
|
25
|
+
}
|
|
26
|
+
.opal-segmented::after {
|
|
27
|
+
content: ''; position: absolute; inset: 0; border-radius: inherit;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
box-shadow: var(--opal-segmented-rim, var(--_opal-glass-rim));
|
|
30
|
+
opacity: .7;
|
|
31
|
+
}
|
|
32
|
+
.opal-segmented-indicator {
|
|
33
|
+
position: absolute;
|
|
34
|
+
inset-block: 3px;
|
|
35
|
+
inset-inline-start: calc(3px + (100% - 6px) / var(--opal-segment-count) * var(--opal-segment-index));
|
|
36
|
+
width: calc((100% - 6px) / var(--opal-segment-count));
|
|
37
|
+
border-radius: 999px;
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
background: var(--opal-segmented-thumb, #fff);
|
|
40
|
+
box-shadow: 0 1px 3px rgb(0 0 0 / 10%), inset 0 0 0 0.5px rgb(255 255 255 / 18%);
|
|
41
|
+
transition: inset-inline-start 400ms cubic-bezier(0.22, 0.61, 0.36, 1), transform 450ms cubic-bezier(0.22, 0.61, 0.36, 1), background-color 180ms ease, box-shadow 250ms ease;
|
|
42
|
+
}
|
|
43
|
+
.opal-segmented-item {
|
|
44
|
+
position: relative;
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
min-width: 0;
|
|
49
|
+
min-height: 36px;
|
|
50
|
+
padding: 7px 24px;
|
|
51
|
+
border: 0;
|
|
52
|
+
border-radius: 999px;
|
|
53
|
+
background: transparent;
|
|
54
|
+
color: inherit;
|
|
55
|
+
font: inherit;
|
|
56
|
+
line-height: 1.25;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
touch-action: none;
|
|
59
|
+
-webkit-tap-highlight-color: transparent;
|
|
60
|
+
user-select: none;
|
|
61
|
+
-webkit-user-select: none;
|
|
62
|
+
}
|
|
63
|
+
.opal-segmented-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: transform 170ms ease; }
|
|
64
|
+
.opal-segmented-item[aria-checked="true"] { color: var(--opal-segmented-selected-color, inherit); }
|
|
65
|
+
.opal-segmented-item:focus-visible { outline: 2px solid var(--opal-focus-color, #2885ff); outline-offset: 3px; }
|
|
66
|
+
.opal-segmented:is([data-dragging], :has(.opal-segmented-item:active:not(:disabled))) .opal-segmented-indicator {
|
|
67
|
+
transform: translateX(var(--opal-segment-pull, 0px)) scale(calc(1.14 + var(--opal-segment-stretch, 0)), calc(1.45 - var(--opal-segment-stretch, 0) * 0.75));
|
|
68
|
+
background: var(--opal-segmented-lifted, rgb(255 255 255 / 30%));
|
|
69
|
+
z-index: 0;
|
|
70
|
+
-webkit-backdrop-filter: blur(.25px) saturate(110%);
|
|
71
|
+
backdrop-filter: blur(.25px) saturate(110%);
|
|
72
|
+
box-shadow: 0 5px 12px rgb(0 0 0 / 13%), var(--_opal-glass-rim);
|
|
73
|
+
transition-duration: 170ms, 170ms, 140ms, 170ms;
|
|
74
|
+
}
|
|
75
|
+
/* Pointer movement is direct; restoring the normal transition on release
|
|
76
|
+
lets the pill settle onto the nearest enabled segment. */
|
|
77
|
+
.opal-segmented[data-tracking] .opal-segmented-indicator {
|
|
78
|
+
transition-property: transform, background-color, box-shadow;
|
|
79
|
+
transition-duration: 170ms, 140ms, 170ms;
|
|
80
|
+
}
|
|
81
|
+
/* A small overshoot on release gives the resisted pull a soft rebound. */
|
|
82
|
+
.opal-segmented[data-rebounding]:not([data-dragging]) .opal-segmented-indicator {
|
|
83
|
+
transition: inset-inline-start 400ms cubic-bezier(0.22, 0.61, 0.36, 1), transform 600ms cubic-bezier(0.22, 1.5, 0.36, 1), background-color 180ms ease, box-shadow 250ms ease;
|
|
84
|
+
}
|
|
85
|
+
.opal-segmented-item:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
86
|
+
.opal-segmented[data-disabled] { opacity: 0.4; }
|
|
87
|
+
.opal-segmented[data-disabled] .opal-segmented-item { opacity: 1; }
|
|
88
|
+
@media (prefers-reduced-motion: reduce) {
|
|
89
|
+
.opal-segmented .opal-segmented-indicator, .opal-segmented .opal-segmented-label { transition: none !important; transform: none !important; }
|
|
90
|
+
}
|
|
91
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
92
|
+
.opal-segmented { background: var(--opal-segmented-solid-background, #ededed); }
|
|
93
|
+
.opal-segmented .opal-segmented-indicator { z-index: 0; background: var(--opal-segmented-thumb, #fff) !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
|
|
94
|
+
}
|
|
95
|
+
@media (forced-colors: active) {
|
|
96
|
+
.opal-segmented::after { display: none; }
|
|
97
|
+
.opal-segmented { background: Canvas; color: ButtonText; outline: 1px solid ButtonText; }
|
|
98
|
+
.opal-segmented .opal-segmented-indicator { z-index: 0; background: Highlight !important; box-shadow: none !important; transform: none !important; }
|
|
99
|
+
.opal-segmented-item[aria-checked="true"] { color: HighlightText; }
|
|
100
|
+
.opal-segmented-item:disabled { color: GrayText; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* An inert visual layer owns the ink; native buttons retain all semantics. */
|
|
104
|
+
.opal-segmented-optical-labels { position:absolute; inset:-24px; padding:27px; display:grid; grid-template-columns:repeat(var(--opal-segment-count),minmax(0,1fr)); pointer-events:none; visibility:hidden; z-index:1; }
|
|
105
|
+
.opal-segmented-optical-label { display:flex; align-items:center; justify-content:center; min-width:0; white-space:nowrap; }
|
|
106
|
+
.opal-segmented-optical-label[data-selected] { color:var(--opal-segmented-selected-color,inherit); }
|
|
107
|
+
.opal-segmented-optical-label[data-disabled] { opacity:.4; }
|
|
108
|
+
.opal-segmented[data-lensing] .opal-segmented-optical-labels { visibility:visible; }
|
|
109
|
+
.opal-segmented[data-lensing] .opal-segmented-label { opacity:0; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default ".opal-segmented-filter { position: absolute; pointer-events: none; }\n.opal-segmented {\n position: relative;\n display: inline-grid;\n grid-template-columns: repeat(var(--opal-segment-count), minmax(0, 1fr));\n box-sizing: border-box;\n padding: 3px;\n border-radius: 999px;\n background: var(--opal-segmented-background, rgb(0 0 0 / 5%));\n color: var(--opal-segmented-color, #202020);\n box-shadow: var(--opal-segmented-shadow, var(--_opal-glass-shadow));\n vertical-align: middle;\n max-width: 100%;\n isolation: isolate;\n}\n.opal-segmented::after {\n content: ''; position: absolute; inset: 0; border-radius: inherit;\n pointer-events: none;\n box-shadow: var(--opal-segmented-rim, var(--_opal-glass-rim));\n opacity: .7;\n}\n.opal-segmented-indicator {\n position: absolute;\n inset-block: 3px;\n inset-inline-start: calc(3px + (100% - 6px) / var(--opal-segment-count) * var(--opal-segment-index));\n width: calc((100% - 6px) / var(--opal-segment-count));\n border-radius: 999px;\n pointer-events: none;\n background: var(--opal-segmented-thumb, #fff);\n box-shadow: 0 1px 3px rgb(0 0 0 / 10%), inset 0 0 0 0.5px rgb(255 255 255 / 18%);\n transition: inset-inline-start 400ms cubic-bezier(0.22, 0.61, 0.36, 1), transform 450ms cubic-bezier(0.22, 0.61, 0.36, 1), background-color 180ms ease, box-shadow 250ms ease;\n}\n.opal-segmented-item {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n min-height: 36px;\n padding: 7px 24px;\n border: 0;\n border-radius: 999px;\n background: transparent;\n color: inherit;\n font: inherit;\n line-height: 1.25;\n cursor: pointer;\n touch-action: none;\n -webkit-tap-highlight-color: transparent;\n user-select: none;\n -webkit-user-select: none;\n}\n.opal-segmented-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: transform 170ms ease; }\n.opal-segmented-item[aria-checked=\"true\"] { color: var(--opal-segmented-selected-color, inherit); }\n.opal-segmented-item:focus-visible { outline: 2px solid var(--opal-focus-color, #2885ff); outline-offset: 3px; }\n.opal-segmented:is([data-dragging], :has(.opal-segmented-item:active:not(:disabled))) .opal-segmented-indicator {\n transform: translateX(var(--opal-segment-pull, 0px)) scale(calc(1.14 + var(--opal-segment-stretch, 0)), calc(1.45 - var(--opal-segment-stretch, 0) * 0.75));\n background: var(--opal-segmented-lifted, rgb(255 255 255 / 30%));\n z-index: 0;\n -webkit-backdrop-filter: blur(.25px) saturate(110%);\n backdrop-filter: blur(.25px) saturate(110%);\n box-shadow: 0 5px 12px rgb(0 0 0 / 13%), var(--_opal-glass-rim);\n transition-duration: 170ms, 170ms, 140ms, 170ms;\n}\n/* Pointer movement is direct; restoring the normal transition on release\n lets the pill settle onto the nearest enabled segment. */\n.opal-segmented[data-tracking] .opal-segmented-indicator {\n transition-property: transform, background-color, box-shadow;\n transition-duration: 170ms, 140ms, 170ms;\n}\n/* A small overshoot on release gives the resisted pull a soft rebound. */\n.opal-segmented[data-rebounding]:not([data-dragging]) .opal-segmented-indicator {\n transition: inset-inline-start 400ms cubic-bezier(0.22, 0.61, 0.36, 1), transform 600ms cubic-bezier(0.22, 1.5, 0.36, 1), background-color 180ms ease, box-shadow 250ms ease;\n}\n.opal-segmented-item:disabled { opacity: 0.4; cursor: not-allowed; }\n.opal-segmented[data-disabled] { opacity: 0.4; }\n.opal-segmented[data-disabled] .opal-segmented-item { opacity: 1; }\n@media (prefers-reduced-motion: reduce) {\n .opal-segmented .opal-segmented-indicator, .opal-segmented .opal-segmented-label { transition: none !important; transform: none !important; }\n}\n@media (prefers-reduced-transparency: reduce) {\n .opal-segmented { background: var(--opal-segmented-solid-background, #ededed); }\n .opal-segmented .opal-segmented-indicator { z-index: 0; background: var(--opal-segmented-thumb, #fff) !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }\n}\n@media (forced-colors: active) {\n .opal-segmented::after { display: none; }\n .opal-segmented { background: Canvas; color: ButtonText; outline: 1px solid ButtonText; }\n .opal-segmented .opal-segmented-indicator { z-index: 0; background: Highlight !important; box-shadow: none !important; transform: none !important; }\n .opal-segmented-item[aria-checked=\"true\"] { color: HighlightText; }\n .opal-segmented-item:disabled { color: GrayText; }\n}\n\n/* An inert visual layer owns the ink; native buttons retain all semantics. */\n.opal-segmented-optical-labels { position:absolute; inset:-24px; padding:27px; display:grid; grid-template-columns:repeat(var(--opal-segment-count),minmax(0,1fr)); pointer-events:none; visibility:hidden; z-index:1; }\n.opal-segmented-optical-label { display:flex; align-items:center; justify-content:center; min-width:0; white-space:nowrap; }\n.opal-segmented-optical-label[data-selected] { color:var(--opal-segmented-selected-color,inherit); }\n.opal-segmented-optical-label[data-disabled] { opacity:.4; }\n.opal-segmented[data-lensing] .opal-segmented-optical-labels { visibility:visible; }\n.opal-segmented[data-lensing] .opal-segmented-label { opacity:0; }\n";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
+
export type SegmentedControlItem = {
|
|
3
|
+
value: string;
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type SegmentedControlProps = Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'defaultValue' | 'onChange' | 'role'> & {
|
|
8
|
+
items: readonly SegmentedControlItem[];
|
|
9
|
+
value?: string;
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
onValueChange?: (value: string) => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
};
|
|
14
|
+
/** A single-choice control. Supply aria-label or aria-labelledby for its group. */
|
|
15
|
+
export declare const SegmentedControl: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "defaultValue" | "role" | "children" | "onChange"> & {
|
|
16
|
+
items: readonly SegmentedControlItem[];
|
|
17
|
+
value?: string;
|
|
18
|
+
defaultValue?: string;
|
|
19
|
+
onValueChange?: (value: string) => void;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
} & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { stylesheetHref } from '../../internal/styles.js';
|
|
5
|
+
import { useGlassLens } from '../../internal/glass/lens.js';
|
|
6
|
+
import { materials } from '../../internal/glass/material.js';
|
|
7
|
+
import componentCss from './segmented-control.css.js';
|
|
8
|
+
import glassCss from '../../internal/glass/styles.js';
|
|
9
|
+
const css = `${glassCss}\n${componentCss}`;
|
|
10
|
+
const styleHref = stylesheetHref('segmented-control', css);
|
|
11
|
+
/** A single-choice control. Supply aria-label or aria-labelledby for its group. */
|
|
12
|
+
export const SegmentedControl = forwardRef(function SegmentedControl({ items, value, defaultValue, onValueChange, disabled = false, className, style, ...props }, ref) {
|
|
13
|
+
const [internalValue, setInternalValue] = useState(() => defaultValue ?? items.find(item => !item.disabled)?.value);
|
|
14
|
+
const [pressed, setPressed] = useState();
|
|
15
|
+
const [dragPosition, setDragPosition] = useState();
|
|
16
|
+
const [overdrag, setOverdrag] = useState({ stretch: 0, offset: 0 });
|
|
17
|
+
const [rebounding, setRebounding] = useState(false);
|
|
18
|
+
const indicator = useRef(null);
|
|
19
|
+
const labelSource = useRef(null);
|
|
20
|
+
const glass = useGlassLens(indicator, materials.selection, { source: labelSource, active: pressed !== undefined && !disabled, ink: true });
|
|
21
|
+
const pointerClick = useRef(null);
|
|
22
|
+
const drag = useRef(null);
|
|
23
|
+
function nearestEnabled(position) {
|
|
24
|
+
return items.reduce((best, item, i) => item.disabled ? best
|
|
25
|
+
: best < 0 || Math.abs(i - position) < Math.abs(best - position) ? i : best, -1);
|
|
26
|
+
}
|
|
27
|
+
function movePill(clientX) {
|
|
28
|
+
const active = drag.current;
|
|
29
|
+
if (!active || disabled)
|
|
30
|
+
return;
|
|
31
|
+
const group = active.target.parentElement;
|
|
32
|
+
if (!group)
|
|
33
|
+
return;
|
|
34
|
+
const width = active.target.getBoundingClientRect().width;
|
|
35
|
+
if (!width)
|
|
36
|
+
return;
|
|
37
|
+
const direction = getComputedStyle(group).direction === 'rtl' ? -1 : 1;
|
|
38
|
+
const rawPosition = active.startIndex + direction * (clientX - active.startX) / width;
|
|
39
|
+
const position = Math.max(0, Math.min(items.length - 1, rawPosition));
|
|
40
|
+
const excess = rawPosition - position;
|
|
41
|
+
// Increasing resistance caps the stretch at 14% of a segment's width.
|
|
42
|
+
// Shift slightly beyond half the extension so the whole pill drifts with the pull.
|
|
43
|
+
const stretch = 0.14 * (1 - Math.exp(-Math.abs(excess) / 0.9));
|
|
44
|
+
setOverdrag({ stretch, offset: direction * Math.sign(excess) * stretch * width * 0.85 });
|
|
45
|
+
active.position = position;
|
|
46
|
+
active.stretch = stretch;
|
|
47
|
+
setDragPosition(position);
|
|
48
|
+
const nearest = nearestEnabled(position);
|
|
49
|
+
setPressed(items[nearest]?.value);
|
|
50
|
+
return stretch;
|
|
51
|
+
}
|
|
52
|
+
function endDrag() {
|
|
53
|
+
const active = drag.current;
|
|
54
|
+
drag.current = null;
|
|
55
|
+
setPressed(undefined);
|
|
56
|
+
setDragPosition(undefined);
|
|
57
|
+
setOverdrag({ stretch: 0, offset: 0 });
|
|
58
|
+
setRebounding(false);
|
|
59
|
+
if (active?.target.hasPointerCapture(active.pointerId))
|
|
60
|
+
active.target.releasePointerCapture(active.pointerId);
|
|
61
|
+
}
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
window.addEventListener('blur', endDrag);
|
|
64
|
+
return () => window.removeEventListener('blur', endDrag);
|
|
65
|
+
}, []);
|
|
66
|
+
useEffect(() => { if (disabled)
|
|
67
|
+
endDrag(); }, [disabled]);
|
|
68
|
+
const selected = value === undefined ? internalValue : value;
|
|
69
|
+
const selectedIndex = items.findIndex(item => item.value === selected);
|
|
70
|
+
const pressedIndex = items.findIndex(item => item.value === pressed && !item.disabled);
|
|
71
|
+
const index = dragPosition ?? (pressedIndex >= 0 ? pressedIndex : selectedIndex);
|
|
72
|
+
const firstEnabled = items.findIndex(item => !item.disabled);
|
|
73
|
+
const focusIndex = selectedIndex >= 0 && !items[selectedIndex]?.disabled ? selectedIndex : firstEnabled;
|
|
74
|
+
function select(next) {
|
|
75
|
+
if (disabled || items.find(item => item.value === next)?.disabled || next === selected)
|
|
76
|
+
return;
|
|
77
|
+
if (value === undefined)
|
|
78
|
+
setInternalValue(next);
|
|
79
|
+
onValueChange?.(next);
|
|
80
|
+
}
|
|
81
|
+
function finishDrag(pointerId, clientX) {
|
|
82
|
+
const active = drag.current;
|
|
83
|
+
if (!active || active.pointerId !== pointerId)
|
|
84
|
+
return;
|
|
85
|
+
// Cancellation/capture loss may carry empty coordinates; keep the last
|
|
86
|
+
// tracked position in those cases instead of reverting to the press origin.
|
|
87
|
+
if (clientX !== undefined)
|
|
88
|
+
movePill(clientX);
|
|
89
|
+
const nextIndex = nearestEnabled(active.position);
|
|
90
|
+
const next = items[nextIndex];
|
|
91
|
+
const button = active.target.parentElement?.querySelectorAll('.opal-segmented-item')[nextIndex];
|
|
92
|
+
pointerClick.current = { target: active.target, until: Date.now() + 500 };
|
|
93
|
+
endDrag();
|
|
94
|
+
if (next)
|
|
95
|
+
select(next.value);
|
|
96
|
+
setRebounding(active.stretch > 0.005);
|
|
97
|
+
button?.focus({ preventScroll: true });
|
|
98
|
+
}
|
|
99
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { href: styleHref, precedence: "opalui", children: css }), glass.filter, _jsxs("div", { ...props, ref: ref, role: "radiogroup", "aria-disabled": disabled || undefined, className: ['opal-segmented', className].filter(Boolean).join(' '), "data-lensing": glass.lensing || undefined, "data-disabled": disabled || undefined, "data-dragging": pressed !== undefined && !disabled || undefined, "data-tracking": dragPosition !== undefined && !disabled || undefined, "data-rebounding": rebounding || undefined, style: { '--opal-segment-count': Math.max(1, items.length), '--opal-segment-index': Math.max(0, index),
|
|
100
|
+
'--opal-segment-stretch': overdrag.stretch, '--opal-segment-pull': `${overdrag.offset}px`, ...glass.surfaceStyle, ...style }, children: [index >= 0 && _jsx("span", { ref: indicator, className: "opal-segmented-indicator", "aria-hidden": "true", onTransitionEnd: event => { if (event.propertyName === 'transform')
|
|
101
|
+
setRebounding(false); } }), _jsx("span", { ref: labelSource, className: "opal-segmented-optical-labels", "aria-hidden": "true", style: glass.sourceStyle, children: items.map(item => _jsx("span", { className: "opal-segmented-optical-label", "data-selected": item.value === selected || undefined, "data-disabled": item.disabled || undefined, children: item.label }, item.value)) }), items.map((item, itemIndex) => _jsx("button", { type: "button", role: "radio", "aria-checked": item.value === selected, disabled: disabled || item.disabled, tabIndex: !disabled && itemIndex === focusIndex ? 0 : -1, className: "opal-segmented-item", "data-pressed": pressed === item.value || undefined, onPointerDown: event => {
|
|
102
|
+
if (event.button !== 0 || !event.isPrimary || drag.current || disabled)
|
|
103
|
+
return;
|
|
104
|
+
setRebounding(false);
|
|
105
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
106
|
+
drag.current = { pointerId: event.pointerId, startX: event.clientX, startIndex: itemIndex, position: itemIndex, stretch: 0, target: event.currentTarget };
|
|
107
|
+
setPressed(item.value);
|
|
108
|
+
select(item.value);
|
|
109
|
+
}, onPointerMove: event => {
|
|
110
|
+
const active = drag.current;
|
|
111
|
+
if (!active || active.pointerId !== event.pointerId || disabled)
|
|
112
|
+
return;
|
|
113
|
+
// Fractional segment positions follow the pointer without selecting
|
|
114
|
+
// intermediate values. The initial grab offset is preserved.
|
|
115
|
+
movePill(event.clientX);
|
|
116
|
+
}, onPointerUp: event => {
|
|
117
|
+
// A fast release can arrive without a final pointermove.
|
|
118
|
+
finishDrag(event.pointerId, event.clientX);
|
|
119
|
+
}, onPointerCancel: event => finishDrag(event.pointerId), onLostPointerCapture: event => finishDrag(event.pointerId), onDragStart: event => event.preventDefault(), onClick: event => {
|
|
120
|
+
setPressed(undefined);
|
|
121
|
+
const pending = pointerClick.current;
|
|
122
|
+
pointerClick.current = null;
|
|
123
|
+
if (pending?.target === event.currentTarget && Date.now() < pending.until)
|
|
124
|
+
return;
|
|
125
|
+
// Pointer selection already happened on press. Keep keyboard and
|
|
126
|
+
// assistive-technology clicks working without a second callback.
|
|
127
|
+
if (event.detail === 0)
|
|
128
|
+
select(item.value);
|
|
129
|
+
}, onKeyDown: event => {
|
|
130
|
+
pointerClick.current = null;
|
|
131
|
+
if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key))
|
|
132
|
+
return;
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
const group = event.currentTarget.parentElement;
|
|
135
|
+
const rtl = getComputedStyle(group).direction === 'rtl';
|
|
136
|
+
const enabled = items.map((entry, i) => entry.disabled ? -1 : i).filter(i => i >= 0);
|
|
137
|
+
const position = enabled.indexOf(itemIndex);
|
|
138
|
+
const backward = event.key === 'ArrowUp' || event.key === (rtl ? 'ArrowRight' : 'ArrowLeft');
|
|
139
|
+
const next = event.key === 'Home' ? enabled[0] : event.key === 'End' ? enabled.at(-1)
|
|
140
|
+
: enabled[(position + (backward ? -1 : 1) + enabled.length) % enabled.length];
|
|
141
|
+
if (next === undefined)
|
|
142
|
+
return;
|
|
143
|
+
select(items[next].value);
|
|
144
|
+
group.querySelectorAll('.opal-segmented-item')[next]?.focus();
|
|
145
|
+
}, children: _jsx("span", { className: "opal-segmented-label", children: item.label }) }, item.value))] })] });
|
|
146
|
+
});
|
|
147
|
+
//# sourceMappingURL=segmented-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segmented-control.js","sourceRoot":"","sources":["../../../src/components/segmented-control/segmented-control.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport { forwardRef, useEffect, useRef, useState, type ComponentPropsWithoutRef, type CSSProperties, type ReactNode } from 'react'\nimport { stylesheetHref } from '../../internal/styles.js'\nimport { useGlassLens } from '../../internal/glass/lens.js'\nimport { materials } from '../../internal/glass/material.js'\nimport componentCss from './segmented-control.css?inline'\nimport glassCss from '../../internal/glass/styles.js'\nconst css = `${glassCss}\\n${componentCss}`\n\nconst styleHref = stylesheetHref('segmented-control', css)\n\nexport type SegmentedControlItem = { value: string; label: ReactNode; disabled?: boolean }\nexport type SegmentedControlProps = Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'defaultValue' | 'onChange' | 'role'> & {\n items: readonly SegmentedControlItem[]\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n disabled?: boolean\n}\n\n/** A single-choice control. Supply aria-label or aria-labelledby for its group. */\nexport const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(function SegmentedControl(\n { items, value, defaultValue, onValueChange, disabled = false, className, style, ...props }, ref,\n) {\n const [internalValue, setInternalValue] = useState(() => defaultValue ?? items.find(item => !item.disabled)?.value)\n const [pressed, setPressed] = useState<string>()\n const [dragPosition, setDragPosition] = useState<number>()\n const [overdrag, setOverdrag] = useState({ stretch: 0, offset: 0 })\n const [rebounding, setRebounding] = useState(false)\n const indicator = useRef<HTMLSpanElement>(null)\n const labelSource = useRef<HTMLSpanElement>(null)\n const glass = useGlassLens(indicator, materials.selection, { source: labelSource, active: pressed !== undefined && !disabled, ink: true })\n const pointerClick = useRef<{ target: HTMLButtonElement; until: number } | null>(null)\n const drag = useRef<{ pointerId: number; startX: number; startIndex: number; position: number; stretch: number; target: HTMLButtonElement } | null>(null)\n\n function nearestEnabled(position: number) {\n return items.reduce((best, item, i) => item.disabled ? best\n : best < 0 || Math.abs(i - position) < Math.abs(best - position) ? i : best, -1)\n }\n\n function movePill(clientX: number) {\n const active = drag.current\n if (!active || disabled) return\n const group = active.target.parentElement\n if (!group) return\n const width = active.target.getBoundingClientRect().width\n if (!width) return\n const direction = getComputedStyle(group).direction === 'rtl' ? -1 : 1\n const rawPosition = active.startIndex + direction * (clientX - active.startX) / width\n const position = Math.max(0, Math.min(items.length - 1, rawPosition))\n const excess = rawPosition - position\n // Increasing resistance caps the stretch at 14% of a segment's width.\n // Shift slightly beyond half the extension so the whole pill drifts with the pull.\n const stretch = 0.14 * (1 - Math.exp(-Math.abs(excess) / 0.9))\n setOverdrag({ stretch, offset: direction * Math.sign(excess) * stretch * width * 0.85 })\n active.position = position\n active.stretch = stretch\n setDragPosition(position)\n const nearest = nearestEnabled(position)\n setPressed(items[nearest]?.value)\n return stretch\n }\n function endDrag() {\n const active = drag.current\n drag.current = null\n setPressed(undefined)\n setDragPosition(undefined)\n setOverdrag({ stretch: 0, offset: 0 })\n setRebounding(false)\n if (active?.target.hasPointerCapture(active.pointerId)) active.target.releasePointerCapture(active.pointerId)\n }\n useEffect(() => {\n window.addEventListener('blur', endDrag)\n return () => window.removeEventListener('blur', endDrag)\n }, [])\n useEffect(() => { if (disabled) endDrag() }, [disabled])\n const selected = value === undefined ? internalValue : value\n const selectedIndex = items.findIndex(item => item.value === selected)\n const pressedIndex = items.findIndex(item => item.value === pressed && !item.disabled)\n const index = dragPosition ?? (pressedIndex >= 0 ? pressedIndex : selectedIndex)\n const firstEnabled = items.findIndex(item => !item.disabled)\n const focusIndex = selectedIndex >= 0 && !items[selectedIndex]?.disabled ? selectedIndex : firstEnabled\n\n function select(next: string) {\n if (disabled || items.find(item => item.value === next)?.disabled || next === selected) return\n if (value === undefined) setInternalValue(next)\n onValueChange?.(next)\n }\n\n function finishDrag(pointerId: number, clientX?: number) {\n const active = drag.current\n if (!active || active.pointerId !== pointerId) return\n // Cancellation/capture loss may carry empty coordinates; keep the last\n // tracked position in those cases instead of reverting to the press origin.\n if (clientX !== undefined) movePill(clientX)\n const nextIndex = nearestEnabled(active.position)\n const next = items[nextIndex]\n const button = active.target.parentElement?.querySelectorAll<HTMLButtonElement>('.opal-segmented-item')[nextIndex]\n pointerClick.current = { target: active.target, until: Date.now() + 500 }\n endDrag()\n if (next) select(next.value)\n setRebounding(active.stretch > 0.005)\n button?.focus({ preventScroll: true })\n }\n\n return <>\n <style href={styleHref} precedence=\"opalui\">{css}</style>\n {glass.filter}\n <div {...props} ref={ref} role=\"radiogroup\" aria-disabled={disabled || undefined}\n className={['opal-segmented', className].filter(Boolean).join(' ')}\n data-lensing={glass.lensing || undefined}\n data-disabled={disabled || undefined}\n data-dragging={pressed !== undefined && !disabled || undefined}\n data-tracking={dragPosition !== undefined && !disabled || undefined}\n data-rebounding={rebounding || undefined}\n style={{ '--opal-segment-count': Math.max(1, items.length), '--opal-segment-index': Math.max(0, index),\n '--opal-segment-stretch': overdrag.stretch, '--opal-segment-pull': `${overdrag.offset}px`, ...glass.surfaceStyle, ...style } as CSSProperties}>\n {index >= 0 && <span ref={indicator} className=\"opal-segmented-indicator\" aria-hidden=\"true\"\n onTransitionEnd={event => { if (event.propertyName === 'transform') setRebounding(false) }} />}\n <span ref={labelSource} className=\"opal-segmented-optical-labels\" aria-hidden=\"true\" style={glass.sourceStyle}>\n {items.map(item => <span key={item.value} className=\"opal-segmented-optical-label\" data-selected={item.value === selected || undefined} data-disabled={item.disabled || undefined}>{item.label}</span>)}\n </span>\n {items.map((item, itemIndex) => <button key={item.value} type=\"button\" role=\"radio\"\n aria-checked={item.value === selected} disabled={disabled || item.disabled}\n tabIndex={!disabled && itemIndex === focusIndex ? 0 : -1}\n className=\"opal-segmented-item\"\n data-pressed={pressed === item.value || undefined}\n onPointerDown={event => {\n if (event.button !== 0 || !event.isPrimary || drag.current || disabled) return\n setRebounding(false)\n event.currentTarget.setPointerCapture(event.pointerId)\n drag.current = { pointerId: event.pointerId, startX: event.clientX, startIndex: itemIndex, position: itemIndex, stretch: 0, target: event.currentTarget }\n setPressed(item.value)\n select(item.value)\n }}\n onPointerMove={event => {\n const active = drag.current\n if (!active || active.pointerId !== event.pointerId || disabled) return\n // Fractional segment positions follow the pointer without selecting\n // intermediate values. The initial grab offset is preserved.\n movePill(event.clientX)\n }}\n onPointerUp={event => {\n // A fast release can arrive without a final pointermove.\n finishDrag(event.pointerId, event.clientX)\n }}\n onPointerCancel={event => finishDrag(event.pointerId)}\n onLostPointerCapture={event => finishDrag(event.pointerId)}\n onDragStart={event => event.preventDefault()}\n onClick={event => {\n setPressed(undefined)\n const pending = pointerClick.current\n pointerClick.current = null\n if (pending?.target === event.currentTarget && Date.now() < pending.until) return\n // Pointer selection already happened on press. Keep keyboard and\n // assistive-technology clicks working without a second callback.\n if (event.detail === 0) select(item.value)\n }}\n onKeyDown={event => {\n pointerClick.current = null\n if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return\n event.preventDefault()\n const group = event.currentTarget.parentElement!\n const rtl = getComputedStyle(group).direction === 'rtl'\n const enabled = items.map((entry, i) => entry.disabled ? -1 : i).filter(i => i >= 0)\n const position = enabled.indexOf(itemIndex)\n const backward = event.key === 'ArrowUp' || event.key === (rtl ? 'ArrowRight' : 'ArrowLeft')\n const next = event.key === 'Home' ? enabled[0] : event.key === 'End' ? enabled.at(-1)\n : enabled[(position + (backward ? -1 : 1) + enabled.length) % enabled.length]\n if (next === undefined) return\n select(items[next]!.value)\n group.querySelectorAll<HTMLButtonElement>('.opal-segmented-item')[next]?.focus()\n }}>\n <span className=\"opal-segmented-label\">{item.label}</span>\n </button>)}\n </div>\n </>\n})\n"]}
|