@octanejs/aria 0.0.4 → 0.0.5
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 +7 -5
- package/src/autocomplete/useAutocomplete.ts +656 -0
- package/src/collections/BaseCollection.ts +374 -0
- package/src/collections/CollectionBuilder.ts +343 -0
- package/src/collections/Document.ts +451 -0
- package/src/collections/Hidden.ts +84 -0
- package/src/collections/useCachedChildren.ts +103 -0
- package/src/components/Autocomplete.ts +124 -0
- package/src/components/Breadcrumbs.ts +170 -0
- package/src/components/Button.ts +209 -0
- package/src/components/Checkbox.ts +593 -0
- package/src/components/Collection.ts +301 -0
- package/src/components/ComboBox.ts +477 -0
- package/src/components/Dialog.ts +195 -0
- package/src/components/Disclosure.ts +322 -0
- package/src/components/DragAndDrop.ts +177 -0
- package/src/components/FieldError.ts +79 -0
- package/src/components/Form.ts +65 -0
- package/src/components/GridList.ts +1099 -0
- package/src/components/Group.ts +146 -0
- package/src/components/Header.ts +32 -0
- package/src/components/Heading.ts +45 -0
- package/src/components/Input.ts +139 -0
- package/src/components/Keyboard.ts +23 -0
- package/src/components/Label.ts +30 -0
- package/src/components/Link.ts +137 -0
- package/src/components/ListBox.ts +886 -0
- package/src/components/Menu.ts +685 -0
- package/src/components/Meter.ts +100 -0
- package/src/components/Modal.ts +352 -0
- package/src/components/NumberField.ts +221 -0
- package/src/components/OverlayArrow.ts +117 -0
- package/src/components/Popover.ts +385 -0
- package/src/components/ProgressBar.ts +112 -0
- package/src/components/RadioGroup.ts +542 -0
- package/src/components/SearchField.ts +208 -0
- package/src/components/Select.ts +505 -0
- package/src/components/SelectionIndicator.ts +52 -0
- package/src/components/Separator.ts +97 -0
- package/src/components/SharedElementTransition.ts +209 -0
- package/src/components/Slider.ts +472 -0
- package/src/components/Switch.ts +449 -0
- package/src/components/Tabs.ts +675 -0
- package/src/components/TagGroup.ts +443 -0
- package/src/components/Text.ts +27 -0
- package/src/components/TextArea.ts +92 -0
- package/src/components/TextField.ts +200 -0
- package/src/components/ToggleButton.ts +147 -0
- package/src/components/ToggleButtonGroup.ts +106 -0
- package/src/components/Toolbar.ts +74 -0
- package/src/components/Tooltip.ts +249 -0
- package/src/components/index.ts +302 -0
- package/src/components/useDragAndDrop.ts +164 -0
- package/src/components/utils.ts +521 -0
- package/src/intl/autocomplete/ar-AE.json +3 -0
- package/src/intl/autocomplete/bg-BG.json +3 -0
- package/src/intl/autocomplete/cs-CZ.json +3 -0
- package/src/intl/autocomplete/da-DK.json +3 -0
- package/src/intl/autocomplete/de-DE.json +3 -0
- package/src/intl/autocomplete/el-GR.json +3 -0
- package/src/intl/autocomplete/en-US.json +3 -0
- package/src/intl/autocomplete/es-ES.json +3 -0
- package/src/intl/autocomplete/et-EE.json +3 -0
- package/src/intl/autocomplete/fi-FI.json +3 -0
- package/src/intl/autocomplete/fr-FR.json +3 -0
- package/src/intl/autocomplete/he-IL.json +3 -0
- package/src/intl/autocomplete/hr-HR.json +3 -0
- package/src/intl/autocomplete/hu-HU.json +3 -0
- package/src/intl/autocomplete/index.ts +75 -0
- package/src/intl/autocomplete/it-IT.json +3 -0
- package/src/intl/autocomplete/ja-JP.json +3 -0
- package/src/intl/autocomplete/ko-KR.json +3 -0
- package/src/intl/autocomplete/lt-LT.json +3 -0
- package/src/intl/autocomplete/lv-LV.json +3 -0
- package/src/intl/autocomplete/nb-NO.json +3 -0
- package/src/intl/autocomplete/nl-NL.json +3 -0
- package/src/intl/autocomplete/pl-PL.json +3 -0
- package/src/intl/autocomplete/pt-BR.json +3 -0
- package/src/intl/autocomplete/pt-PT.json +3 -0
- package/src/intl/autocomplete/ro-RO.json +3 -0
- package/src/intl/autocomplete/ru-RU.json +3 -0
- package/src/intl/autocomplete/sk-SK.json +3 -0
- package/src/intl/autocomplete/sl-SI.json +3 -0
- package/src/intl/autocomplete/sr-SP.json +3 -0
- package/src/intl/autocomplete/sv-SE.json +3 -0
- package/src/intl/autocomplete/tr-TR.json +3 -0
- package/src/intl/autocomplete/uk-UA.json +3 -0
- package/src/intl/autocomplete/zh-CN.json +3 -0
- package/src/intl/autocomplete/zh-TW.json +3 -0
- package/src/stately/autocomplete/useAutocompleteState.ts +85 -0
- package/src/utils/animation.ts +149 -0
- package/src/utils/useLoadMoreSentinel.ts +78 -0
- package/src/utils/useViewportSize.ts +123 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/Autocomplete.tsx).
|
|
2
|
+
// octane adaptations: `.tsx` → `.ts`, JSX → `createElement`; the component is a plain
|
|
3
|
+
// function with the S()/subSlot component-slot convention (no ref — upstream Autocomplete
|
|
4
|
+
// is not a forwardRef component; it renders no DOM of its own). React types → the binding's
|
|
5
|
+
// ported equivalents. `FieldInputContext` keeps the exact Phase-4 value shape the field
|
|
6
|
+
// primitives already consume — TextField/SearchField/NumberField pass it to
|
|
7
|
+
// `useContextProps(props, inputRef, FieldInputContext as any, ...)`: the context's
|
|
8
|
+
// `onChange`/`value` are VALUE-level members of the ported (native-event) AriaTextFieldProps
|
|
9
|
+
// surface, so the merged props flow straight into useTextField, which wires onChange onto
|
|
10
|
+
// octane's NATIVE `input` event. No synthetic onChange anywhere.
|
|
11
|
+
import type {
|
|
12
|
+
AriaLabelingProps,
|
|
13
|
+
DOMProps,
|
|
14
|
+
FocusableElement,
|
|
15
|
+
Node,
|
|
16
|
+
ValueBase,
|
|
17
|
+
} from '@react-types/shared';
|
|
18
|
+
import { createContext, createElement, useRef } from 'octane';
|
|
19
|
+
|
|
20
|
+
import { type AriaAutocompleteProps, useAutocomplete } from '../autocomplete/useAutocomplete';
|
|
21
|
+
// octane adaptation: the ported NATIVE-event handler bags (upstream: FocusEvents/
|
|
22
|
+
// KeyboardEvents from '@react-types/shared', typed over React synthetic events).
|
|
23
|
+
import type { FocusEvents } from '../interactions/useFocus';
|
|
24
|
+
import type { KeyboardEvents } from '../interactions/useKeyboard';
|
|
25
|
+
import type { AriaTextFieldProps } from '../textfield/useTextField';
|
|
26
|
+
import {
|
|
27
|
+
type AutocompleteState,
|
|
28
|
+
useAutocompleteState,
|
|
29
|
+
} from '../stately/autocomplete/useAutocompleteState';
|
|
30
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
31
|
+
import { S, subSlot } from '../internal';
|
|
32
|
+
import {
|
|
33
|
+
type ContextValue,
|
|
34
|
+
Provider,
|
|
35
|
+
removeDataAttributes,
|
|
36
|
+
type SlotProps,
|
|
37
|
+
type SlottedContextValue,
|
|
38
|
+
useSlottedContext,
|
|
39
|
+
} from './utils';
|
|
40
|
+
|
|
41
|
+
export interface AutocompleteProps<T = object> extends AriaAutocompleteProps<T>, SlotProps {}
|
|
42
|
+
export const AutocompleteContext =
|
|
43
|
+
createContext<SlottedContextValue<Partial<AutocompleteProps<any>>>>(null);
|
|
44
|
+
export const AutocompleteStateContext = createContext<AutocompleteState | null>(null);
|
|
45
|
+
|
|
46
|
+
export interface SelectableCollectionContextValue<T> extends DOMProps, AriaLabelingProps {
|
|
47
|
+
filter?: (nodeTextValue: string, node: Node<T>) => boolean;
|
|
48
|
+
/** Whether the collection items should use virtual focus instead of being focused directly. */
|
|
49
|
+
shouldUseVirtualFocus?: boolean;
|
|
50
|
+
/** Whether typeahead is disabled. */
|
|
51
|
+
disallowTypeAhead?: boolean;
|
|
52
|
+
}
|
|
53
|
+
interface FieldInputContextValue<T = FocusableElement>
|
|
54
|
+
extends
|
|
55
|
+
DOMProps,
|
|
56
|
+
FocusEvents<T>,
|
|
57
|
+
KeyboardEvents,
|
|
58
|
+
Pick<ValueBase<string>, 'onChange' | 'value'>,
|
|
59
|
+
Pick<
|
|
60
|
+
AriaTextFieldProps,
|
|
61
|
+
| 'enterKeyHint'
|
|
62
|
+
| 'aria-controls'
|
|
63
|
+
| 'aria-autocomplete'
|
|
64
|
+
| 'aria-activedescendant'
|
|
65
|
+
| 'spellCheck'
|
|
66
|
+
| 'autoCorrect'
|
|
67
|
+
| 'autoComplete'
|
|
68
|
+
> {}
|
|
69
|
+
|
|
70
|
+
export const SelectableCollectionContext =
|
|
71
|
+
createContext<ContextValue<SelectableCollectionContextValue<any>, HTMLElement>>(null);
|
|
72
|
+
export const FieldInputContext =
|
|
73
|
+
createContext<ContextValue<FieldInputContextValue, FocusableElement>>(null);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* An autocomplete allows users to search or filter a list of suggestions.
|
|
77
|
+
*/
|
|
78
|
+
export function Autocomplete<T>(props: AutocompleteProps<T>): any {
|
|
79
|
+
const slot = S('Autocomplete');
|
|
80
|
+
let ctx = useSlottedContext(AutocompleteContext, props.slot);
|
|
81
|
+
props = mergeProps(ctx, props) as AutocompleteProps<T>;
|
|
82
|
+
let { filter, disableAutoFocusFirst } = props;
|
|
83
|
+
let state = useAutocompleteState(props, subSlot(slot, 'state'));
|
|
84
|
+
let inputRef = useRef<HTMLInputElement | null>(null, subSlot(slot, 'inputRef'));
|
|
85
|
+
let collectionRef = useRef<HTMLElement | null>(null, subSlot(slot, 'collectionRef'));
|
|
86
|
+
let {
|
|
87
|
+
inputProps,
|
|
88
|
+
collectionProps,
|
|
89
|
+
collectionRef: mergedCollectionRef,
|
|
90
|
+
filter: filterFn,
|
|
91
|
+
} = useAutocomplete(
|
|
92
|
+
{
|
|
93
|
+
...removeDataAttributes(props),
|
|
94
|
+
filter,
|
|
95
|
+
disableAutoFocusFirst,
|
|
96
|
+
inputRef,
|
|
97
|
+
collectionRef,
|
|
98
|
+
},
|
|
99
|
+
state,
|
|
100
|
+
subSlot(slot, 'autocomplete'),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return createElement(Provider, {
|
|
104
|
+
values: [
|
|
105
|
+
[AutocompleteStateContext, state],
|
|
106
|
+
[
|
|
107
|
+
FieldInputContext,
|
|
108
|
+
{
|
|
109
|
+
...inputProps,
|
|
110
|
+
ref: inputRef,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
SelectableCollectionContext,
|
|
115
|
+
{
|
|
116
|
+
...collectionProps,
|
|
117
|
+
filter: filterFn,
|
|
118
|
+
ref: mergedCollectionRef,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
] as any,
|
|
122
|
+
children: props.children,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/Breadcrumbs.tsx).
|
|
2
|
+
// octane adaptations: `.tsx` → `.ts`, JSX → `createElement`; NO forwardRef — the forwarded ref
|
|
3
|
+
// is `props.ref`, passed into `useContextProps` explicitly and re-applied as the merged object
|
|
4
|
+
// ref; the plain-`.ts` components use the S()/subSlot component-slot convention. The collection
|
|
5
|
+
// composes the Phase-4 engine: `CollectionBuilder`/`createLeafComponent` from `../collections/
|
|
6
|
+
// CollectionBuilder`, the renderer's `CollectionRoot` via `CollectionRendererContext`.
|
|
7
|
+
// Upstream's RAC-local `CollectionProps` import is our `ItemCollectionProps` (see
|
|
8
|
+
// ./Collection.ts). Upstream recreates `useBreadcrumbItem` inline (composition instead of a
|
|
9
|
+
// built-in link) — kept verbatim, feeding `LinkContext`.
|
|
10
|
+
import type { AriaLabelingProps, Key, Node } from '@react-types/shared';
|
|
11
|
+
import { createContext, createElement, useContext } from 'octane';
|
|
12
|
+
|
|
13
|
+
import { type AriaBreadcrumbsProps, useBreadcrumbs } from '../breadcrumbs/useBreadcrumbs';
|
|
14
|
+
import { CollectionNode } from '../collections/BaseCollection';
|
|
15
|
+
import { CollectionBuilder, createLeafComponent } from '../collections/CollectionBuilder';
|
|
16
|
+
import { S, subSlot } from '../internal';
|
|
17
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
18
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
19
|
+
import { Collection, CollectionRendererContext, type ItemCollectionProps } from './Collection';
|
|
20
|
+
import { LinkContext } from './Link';
|
|
21
|
+
import {
|
|
22
|
+
type ClassNameOrFunction,
|
|
23
|
+
type ContextValue,
|
|
24
|
+
dom,
|
|
25
|
+
type DOMRenderProps,
|
|
26
|
+
type RenderProps,
|
|
27
|
+
type SlotProps,
|
|
28
|
+
type StyleProps,
|
|
29
|
+
useContextProps,
|
|
30
|
+
useRenderProps,
|
|
31
|
+
useSlottedContext,
|
|
32
|
+
} from './utils';
|
|
33
|
+
|
|
34
|
+
// octane adaptation: structural bag (upstream's `GlobalDOMAttributes` drags React handler types).
|
|
35
|
+
type GlobalDOMAttributes = Record<string, any>;
|
|
36
|
+
|
|
37
|
+
export interface BreadcrumbsProps<T>
|
|
38
|
+
extends
|
|
39
|
+
Omit<ItemCollectionProps<T>, 'disabledKeys'>,
|
|
40
|
+
AriaBreadcrumbsProps,
|
|
41
|
+
StyleProps,
|
|
42
|
+
SlotProps,
|
|
43
|
+
AriaLabelingProps,
|
|
44
|
+
DOMRenderProps<'ol', undefined>,
|
|
45
|
+
GlobalDOMAttributes {
|
|
46
|
+
/**
|
|
47
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
48
|
+
* element.
|
|
49
|
+
*
|
|
50
|
+
* @default 'react-aria-Breadcrumbs'
|
|
51
|
+
*/
|
|
52
|
+
className?: string;
|
|
53
|
+
/** Whether the breadcrumbs are disabled. */
|
|
54
|
+
isDisabled?: boolean;
|
|
55
|
+
/** Handler that is called when a breadcrumb is clicked. */
|
|
56
|
+
onAction?: (key: Key) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const BreadcrumbsContext =
|
|
60
|
+
createContext<ContextValue<BreadcrumbsProps<any>, HTMLOListElement>>(null);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Breadcrumbs display a hierarchy of links to the current page or resource in an application.
|
|
64
|
+
*/
|
|
65
|
+
export function Breadcrumbs<T extends object>(props: BreadcrumbsProps<T>): any {
|
|
66
|
+
const slot = S('Breadcrumbs');
|
|
67
|
+
let ref: any;
|
|
68
|
+
[props, ref] = useContextProps(props, props.ref, BreadcrumbsContext, subSlot(slot, 'ctx'));
|
|
69
|
+
let { CollectionRoot } = useContext(CollectionRendererContext);
|
|
70
|
+
let { navProps } = useBreadcrumbs(props, subSlot(slot, 'breadcrumbs'));
|
|
71
|
+
let DOMProps = filterDOMProps(props, { global: true, labelable: true });
|
|
72
|
+
|
|
73
|
+
return createElement(CollectionBuilder, {
|
|
74
|
+
content: createElement(Collection, props as any),
|
|
75
|
+
children: (collection: any) =>
|
|
76
|
+
createElement(dom.ol, {
|
|
77
|
+
render: props.render,
|
|
78
|
+
ref,
|
|
79
|
+
...mergeProps(DOMProps, navProps),
|
|
80
|
+
slot: props.slot || undefined,
|
|
81
|
+
style: props.style,
|
|
82
|
+
className: props.className ?? 'react-aria-Breadcrumbs',
|
|
83
|
+
children: createElement(BreadcrumbsContext.Provider, {
|
|
84
|
+
value: props,
|
|
85
|
+
children: createElement(CollectionRoot, { collection }),
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BreadcrumbRenderProps {
|
|
92
|
+
/**
|
|
93
|
+
* Whether the breadcrumb is for the current page.
|
|
94
|
+
*
|
|
95
|
+
* @selector [data-current]
|
|
96
|
+
*/
|
|
97
|
+
isCurrent: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Whether the breadcrumb is disabled.
|
|
100
|
+
*
|
|
101
|
+
* @selector [data-disabled]
|
|
102
|
+
*/
|
|
103
|
+
isDisabled: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface BreadcrumbProps
|
|
107
|
+
extends RenderProps<BreadcrumbRenderProps, 'li'>, AriaLabelingProps, GlobalDOMAttributes {
|
|
108
|
+
/**
|
|
109
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
110
|
+
* element. A function may be provided to compute the class based on component state.
|
|
111
|
+
*
|
|
112
|
+
* @default 'react-aria-Breadcrumb'
|
|
113
|
+
*/
|
|
114
|
+
className?: ClassNameOrFunction<BreadcrumbRenderProps>;
|
|
115
|
+
/**
|
|
116
|
+
* A unique id for the breadcrumb, which will be passed to `onAction` when the breadcrumb is
|
|
117
|
+
* pressed.
|
|
118
|
+
*/
|
|
119
|
+
id?: Key;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class BreadcrumbNode extends CollectionNode<unknown> {
|
|
123
|
+
static readonly type = 'item';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A Breadcrumb represents an individual item in a `<Breadcrumbs>` list.
|
|
128
|
+
*/
|
|
129
|
+
export const Breadcrumb: (props: BreadcrumbProps & { ref?: any }) => any = createLeafComponent(
|
|
130
|
+
BreadcrumbNode,
|
|
131
|
+
// The third (node) parameter is declared so `render.length === 3` keeps the
|
|
132
|
+
// engine's "cannot be rendered outside a collection" guard; it is always
|
|
133
|
+
// provided when rendered from a collection node.
|
|
134
|
+
function Breadcrumb(props: BreadcrumbProps, ref: any, node?: Node<unknown>): any {
|
|
135
|
+
const slot = S('Breadcrumb');
|
|
136
|
+
// Recreating useBreadcrumbItem because we want to use composition instead of having the link builtin.
|
|
137
|
+
let isCurrent = node!.nextKey == null;
|
|
138
|
+
let { isDisabled, onAction } = useSlottedContext(BreadcrumbsContext)!;
|
|
139
|
+
let linkProps = {
|
|
140
|
+
'aria-current': isCurrent ? 'page' : null,
|
|
141
|
+
isDisabled: isDisabled || isCurrent,
|
|
142
|
+
onPress: () => onAction?.(node!.key),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let renderProps = useRenderProps(
|
|
146
|
+
{
|
|
147
|
+
...node!.props,
|
|
148
|
+
children: node!.rendered,
|
|
149
|
+
values: { isDisabled: isDisabled || isCurrent, isCurrent },
|
|
150
|
+
defaultClassName: 'react-aria-Breadcrumb',
|
|
151
|
+
} as any,
|
|
152
|
+
subSlot(slot, 'render'),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
let DOMProps = filterDOMProps(props as any, { global: true, labelable: true });
|
|
156
|
+
delete DOMProps.id;
|
|
157
|
+
|
|
158
|
+
return createElement(dom.li, {
|
|
159
|
+
...DOMProps,
|
|
160
|
+
...renderProps,
|
|
161
|
+
ref,
|
|
162
|
+
'data-disabled': isDisabled || isCurrent || undefined,
|
|
163
|
+
'data-current': isCurrent || undefined,
|
|
164
|
+
children: createElement(LinkContext.Provider, {
|
|
165
|
+
value: linkProps as any,
|
|
166
|
+
children: renderProps.children,
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
);
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/Button.tsx).
|
|
2
|
+
// octane adaptations: `.tsx` → `.ts`, JSX → `createElement`; NO forwardRef — the forwarded ref
|
|
3
|
+
// arrives positionally from `createHideableComponent` (which forwards `props.ref`); the
|
|
4
|
+
// plain-`.ts` component uses the S()/subSlot component-slot convention; `announce` comes from
|
|
5
|
+
// the binding's live-announcer port; the explicit dep array on the pending-announcement effect
|
|
6
|
+
// is preserved verbatim.
|
|
7
|
+
import type { HoverEvents } from '@react-types/shared';
|
|
8
|
+
import { createContext, createElement, useEffect, useRef } from 'octane';
|
|
9
|
+
|
|
10
|
+
import { type AriaButtonProps, useButton } from '../button/useButton';
|
|
11
|
+
import { createHideableComponent } from '../collections/Hidden';
|
|
12
|
+
import { useFocusRing } from '../focus/useFocusRing';
|
|
13
|
+
import { useHover } from '../interactions/useHover';
|
|
14
|
+
import { S, subSlot } from '../internal';
|
|
15
|
+
import { announce } from '../live-announcer/LiveAnnouncer';
|
|
16
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
17
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
18
|
+
import { useId } from '../utils/useId';
|
|
19
|
+
import { ProgressBarContext } from './ProgressBar';
|
|
20
|
+
import {
|
|
21
|
+
type ClassNameOrFunction,
|
|
22
|
+
type ContextValue,
|
|
23
|
+
dom,
|
|
24
|
+
type RenderProps,
|
|
25
|
+
type SlotProps,
|
|
26
|
+
useContextProps,
|
|
27
|
+
useRenderProps,
|
|
28
|
+
} from './utils';
|
|
29
|
+
|
|
30
|
+
// octane adaptation: structural bag (upstream's `GlobalDOMAttributes` drags React handler types).
|
|
31
|
+
type GlobalDOMAttributes = Record<string, any>;
|
|
32
|
+
|
|
33
|
+
export interface ButtonRenderProps {
|
|
34
|
+
/**
|
|
35
|
+
* Whether the button is currently hovered with a mouse.
|
|
36
|
+
*
|
|
37
|
+
* @selector [data-hovered]
|
|
38
|
+
*/
|
|
39
|
+
isHovered: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the button is currently in a pressed state.
|
|
42
|
+
*
|
|
43
|
+
* @selector [data-pressed]
|
|
44
|
+
*/
|
|
45
|
+
isPressed: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the button is focused, either via a mouse or keyboard.
|
|
48
|
+
*
|
|
49
|
+
* @selector [data-focused]
|
|
50
|
+
*/
|
|
51
|
+
isFocused: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Whether the button is keyboard focused.
|
|
54
|
+
*
|
|
55
|
+
* @selector [data-focus-visible]
|
|
56
|
+
*/
|
|
57
|
+
isFocusVisible: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the button is disabled.
|
|
60
|
+
*
|
|
61
|
+
* @selector [data-disabled]
|
|
62
|
+
*/
|
|
63
|
+
isDisabled: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the button is currently in a pending state.
|
|
66
|
+
*
|
|
67
|
+
* @selector [data-pending]
|
|
68
|
+
*/
|
|
69
|
+
isPending: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ButtonProps
|
|
73
|
+
extends
|
|
74
|
+
Omit<AriaButtonProps, 'children' | 'href' | 'target' | 'rel' | 'elementType'>,
|
|
75
|
+
HoverEvents,
|
|
76
|
+
SlotProps,
|
|
77
|
+
RenderProps<ButtonRenderProps, 'button'>,
|
|
78
|
+
Omit<GlobalDOMAttributes, 'onClick'> {
|
|
79
|
+
/**
|
|
80
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
81
|
+
* element. A function may be provided to compute the class based on component state.
|
|
82
|
+
*
|
|
83
|
+
* @default 'react-aria-Button'
|
|
84
|
+
*/
|
|
85
|
+
className?: ClassNameOrFunction<ButtonRenderProps>;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the button is in a pending state. This disables press and hover events
|
|
88
|
+
* while retaining focusability, and announces the pending state to screen readers.
|
|
89
|
+
*/
|
|
90
|
+
isPending?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface ButtonContextValue extends ButtonProps {
|
|
94
|
+
isPressed?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const ButtonContext = createContext<ContextValue<ButtonContextValue, HTMLButtonElement>>({});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A button allows a user to perform an action, with mouse, touch, and keyboard interactions.
|
|
101
|
+
*/
|
|
102
|
+
export const Button = /*#__PURE__*/ createHideableComponent(function Button(
|
|
103
|
+
props: ButtonProps,
|
|
104
|
+
ref: any,
|
|
105
|
+
) {
|
|
106
|
+
const slot = S('Button');
|
|
107
|
+
[props, ref] = useContextProps(props, ref, ButtonContext, subSlot(slot, 'ctx'));
|
|
108
|
+
let ctx = props as ButtonContextValue;
|
|
109
|
+
let { isPending } = ctx;
|
|
110
|
+
let { buttonProps, isPressed } = useButton(props, ref, subSlot(slot, 'button'));
|
|
111
|
+
buttonProps = useDisableInteractions(buttonProps, isPending);
|
|
112
|
+
let { focusProps, isFocused, isFocusVisible } = useFocusRing(props, subSlot(slot, 'focusRing'));
|
|
113
|
+
let { hoverProps, isHovered } = useHover(
|
|
114
|
+
{
|
|
115
|
+
...props,
|
|
116
|
+
isDisabled: props.isDisabled || isPending,
|
|
117
|
+
},
|
|
118
|
+
subSlot(slot, 'hover'),
|
|
119
|
+
);
|
|
120
|
+
let renderValues = {
|
|
121
|
+
isHovered,
|
|
122
|
+
isPressed: (ctx.isPressed || isPressed) && !isPending,
|
|
123
|
+
isFocused,
|
|
124
|
+
isFocusVisible,
|
|
125
|
+
isDisabled: props.isDisabled || false,
|
|
126
|
+
isPending: isPending ?? false,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let renderProps = useRenderProps(
|
|
130
|
+
{
|
|
131
|
+
...props,
|
|
132
|
+
values: renderValues,
|
|
133
|
+
defaultClassName: 'react-aria-Button',
|
|
134
|
+
},
|
|
135
|
+
subSlot(slot, 'render'),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
let buttonId = useId(buttonProps.id, subSlot(slot, 'buttonId'));
|
|
139
|
+
let progressId = useId(undefined, subSlot(slot, 'progressId'));
|
|
140
|
+
|
|
141
|
+
let ariaLabelledby = buttonProps['aria-labelledby'];
|
|
142
|
+
if (isPending) {
|
|
143
|
+
// aria-labelledby wins over aria-label
|
|
144
|
+
// https://www.w3.org/TR/accname-1.2/#computation-steps
|
|
145
|
+
if (ariaLabelledby) {
|
|
146
|
+
ariaLabelledby = `${ariaLabelledby} ${progressId}`;
|
|
147
|
+
} else if (buttonProps['aria-label']) {
|
|
148
|
+
ariaLabelledby = `${buttonId} ${progressId}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let wasPending = useRef(isPending, subSlot(slot, 'wasPending'));
|
|
153
|
+
useEffect(
|
|
154
|
+
() => {
|
|
155
|
+
let message = { 'aria-labelledby': ariaLabelledby || buttonId };
|
|
156
|
+
if (!wasPending.current && isFocused && isPending) {
|
|
157
|
+
announce(message, 'assertive');
|
|
158
|
+
} else if (wasPending.current && isFocused && !isPending) {
|
|
159
|
+
announce(message, 'assertive');
|
|
160
|
+
}
|
|
161
|
+
wasPending.current = isPending;
|
|
162
|
+
},
|
|
163
|
+
[isPending, isFocused, ariaLabelledby, buttonId],
|
|
164
|
+
subSlot(slot, 'announce'),
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
let DOMProps = filterDOMProps(props, { global: true });
|
|
168
|
+
delete DOMProps.onClick;
|
|
169
|
+
|
|
170
|
+
return createElement(dom.button, {
|
|
171
|
+
...mergeProps(DOMProps, renderProps, buttonProps, focusProps, hoverProps),
|
|
172
|
+
// When the button is in a pending state, we want to stop implicit form submission (ie. when the user presses enter on a text input).
|
|
173
|
+
// We do this by changing the button's type to button.
|
|
174
|
+
type: buttonProps.type === 'submit' && isPending ? 'button' : buttonProps.type,
|
|
175
|
+
id: buttonId,
|
|
176
|
+
ref,
|
|
177
|
+
'aria-labelledby': ariaLabelledby,
|
|
178
|
+
slot: props.slot || undefined,
|
|
179
|
+
'aria-disabled': isPending ? 'true' : buttonProps['aria-disabled'],
|
|
180
|
+
'data-disabled': props.isDisabled || undefined,
|
|
181
|
+
'data-pressed': renderValues.isPressed || undefined,
|
|
182
|
+
'data-hovered': isHovered || undefined,
|
|
183
|
+
'data-focused': isFocused || undefined,
|
|
184
|
+
'data-pending': isPending || undefined,
|
|
185
|
+
'data-focus-visible': isFocusVisible || undefined,
|
|
186
|
+
children: createElement(ProgressBarContext.Provider, {
|
|
187
|
+
value: { id: progressId },
|
|
188
|
+
children: renderProps.children,
|
|
189
|
+
}),
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Events to preserve when isPending is true (for tooltips and other overlays)
|
|
194
|
+
const PRESERVED_EVENT_PATTERN =
|
|
195
|
+
/Focus|Blur|Hover|Pointer(Enter|Leave|Over|Out)|Mouse(Enter|Leave|Over|Out)/;
|
|
196
|
+
|
|
197
|
+
// Not a hook (despite the upstream name): a plain per-render prop transform.
|
|
198
|
+
function useDisableInteractions(props: any, isPending: boolean | undefined) {
|
|
199
|
+
if (isPending) {
|
|
200
|
+
for (const key in props) {
|
|
201
|
+
if (key.startsWith('on') && !PRESERVED_EVENT_PATTERN.test(key)) {
|
|
202
|
+
props[key] = undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
props.href = undefined;
|
|
206
|
+
props.target = undefined;
|
|
207
|
+
}
|
|
208
|
+
return props;
|
|
209
|
+
}
|