@allxsmith/bestax-bulma 5.26.3 → 5.26.4
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/bestax.css +1 -1
- package/dist/bestax.css.map +1 -1
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +385 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +385 -123
- package/dist/index.esm.js.map +1 -1
- package/dist/types/form/Checkboxes.d.ts +7 -0
- package/dist/types/form/DateInput.d.ts +18 -3
- package/dist/types/form/DateInputBase.d.ts +17 -3
- package/dist/types/form/DateTimeInput.d.ts +18 -3
- package/dist/types/form/DateTimeInputBase.d.ts +17 -3
- package/dist/types/form/File.d.ts +6 -0
- package/dist/types/form/FormContext.d.ts +39 -7
- package/dist/types/form/Input.d.ts +7 -2
- package/dist/types/form/Radios.d.ts +7 -0
- package/dist/types/form/Rate.d.ts +7 -0
- package/dist/types/form/Select.d.ts +7 -2
- package/dist/types/form/Slider.d.ts +7 -0
- package/dist/types/form/Taginput.d.ts +6 -0
- package/dist/types/form/TextArea.d.ts +7 -2
- package/dist/types/form/TimeInput.d.ts +18 -3
- package/dist/types/form/TimeInputBase.d.ts +16 -3
- package/dist/types/form/_pickerInternals/Calendar.d.ts +9 -3
- package/dist/types/form/_pickerInternals/dateUtils.d.ts +17 -0
- package/dist/types/form/controlLoading.d.ts +4 -0
- package/dist/types/helpers/useAnchoredPosition.d.ts +4 -1
- package/dist/types/helpers/useFocusTrap.d.ts +11 -4
- package/dist/types-cjs/form/Checkboxes.d.ts +7 -0
- package/dist/types-cjs/form/DateInput.d.ts +18 -3
- package/dist/types-cjs/form/DateInputBase.d.ts +17 -3
- package/dist/types-cjs/form/DateTimeInput.d.ts +18 -3
- package/dist/types-cjs/form/DateTimeInputBase.d.ts +17 -3
- package/dist/types-cjs/form/File.d.ts +6 -0
- package/dist/types-cjs/form/FormContext.d.ts +39 -7
- package/dist/types-cjs/form/Input.d.ts +7 -2
- package/dist/types-cjs/form/Radios.d.ts +7 -0
- package/dist/types-cjs/form/Rate.d.ts +7 -0
- package/dist/types-cjs/form/Select.d.ts +7 -2
- package/dist/types-cjs/form/Slider.d.ts +7 -0
- package/dist/types-cjs/form/Taginput.d.ts +6 -0
- package/dist/types-cjs/form/TextArea.d.ts +7 -2
- package/dist/types-cjs/form/TimeInput.d.ts +18 -3
- package/dist/types-cjs/form/TimeInputBase.d.ts +16 -3
- package/dist/types-cjs/form/_pickerInternals/Calendar.d.ts +9 -3
- package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +17 -0
- package/dist/types-cjs/form/controlLoading.d.ts +4 -0
- package/dist/types-cjs/helpers/useAnchoredPosition.d.ts +4 -1
- package/dist/types-cjs/helpers/useFocusTrap.d.ts +11 -4
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/form/_datetimeinput.scss +9 -0
- package/src/scss/form/_picker-popover.scss +12 -9
- package/src/scss/form/_rate.scss +3 -1
- package/src/scss/form/_timeinput.scss +5 -0
package/dist/index.esm.js
CHANGED
|
@@ -5199,10 +5199,17 @@ const focusedElement = (doc) => getDeepestActiveElement(doc) ?? doc.body;
|
|
|
5199
5199
|
* container, or give them a trap of their own. To trap content a `Portal`
|
|
5200
5200
|
* renders, pass a ref to the element inside the `Portal` as `containerRef`.
|
|
5201
5201
|
*
|
|
5202
|
-
* The
|
|
5203
|
-
*
|
|
5204
|
-
*
|
|
5205
|
-
*
|
|
5202
|
+
* The trap looks at `containerRef` again each time the component calling it
|
|
5203
|
+
* renders. A container that mounts after the trap turns on, such as a panel
|
|
5204
|
+
* waiting for a `Portal` target held in state, has it attach once that
|
|
5205
|
+
* component renders with the container in the DOM. When the container leaves
|
|
5206
|
+
* or is replaced, the trap lets go of it as it would turning off, handing
|
|
5207
|
+
* focus back, and attaches to the next one.
|
|
5208
|
+
* A child that mounts the container in a render of its own (from its own
|
|
5209
|
+
* state, or as a `Suspense` boundary resolves) goes unseen until the
|
|
5210
|
+
* component calling the hook renders again. The trap waits for hydration, so
|
|
5211
|
+
* that component renders again when the page hydrates, and portaled content,
|
|
5212
|
+
* which first appears then, is found.
|
|
5206
5213
|
*
|
|
5207
5214
|
* @function useFocusTrap
|
|
5208
5215
|
* @param containerRef - Ref to the element focus stays inside.
|
|
@@ -5222,12 +5229,18 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5222
5229
|
// Portaled content has no server-rendered counterpart, so a portaled
|
|
5223
5230
|
// container only exists from the commit after hydration.
|
|
5224
5231
|
const hydrated = useIsHydrated();
|
|
5232
|
+
// The container the trap is attached to, or `null` while it has none.
|
|
5233
|
+
const heldRef = useRef(null);
|
|
5234
|
+
// Bumped to run the trap again when the container in the ref changes.
|
|
5235
|
+
const [refChanges, setRefChanges] = useState(0);
|
|
5225
5236
|
useEffect(() => {
|
|
5237
|
+
heldRef.current = null;
|
|
5226
5238
|
if (!active || !hydrated)
|
|
5227
5239
|
return undefined;
|
|
5228
5240
|
const container = containerRef.current;
|
|
5229
5241
|
if (!container)
|
|
5230
5242
|
return undefined;
|
|
5243
|
+
heldRef.current = container;
|
|
5231
5244
|
const doc = container.ownerDocument;
|
|
5232
5245
|
const opener = focusedElement(doc);
|
|
5233
5246
|
// `initialFocusRef` counts only when focusing it puts focus inside. Focus
|
|
@@ -5302,7 +5315,28 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5302
5315
|
target.focus();
|
|
5303
5316
|
}
|
|
5304
5317
|
};
|
|
5305
|
-
}, [
|
|
5318
|
+
}, [
|
|
5319
|
+
active,
|
|
5320
|
+
hydrated,
|
|
5321
|
+
containerRef,
|
|
5322
|
+
initialFocusRef,
|
|
5323
|
+
restoreFocus,
|
|
5324
|
+
refChanges,
|
|
5325
|
+
]);
|
|
5326
|
+
// Nothing says when a ref is set, so after every render, a ref that no
|
|
5327
|
+
// longer matches what the trap holds (a container that mounted, left or was
|
|
5328
|
+
// replaced) runs it again. Effects run in order, so whenever the effect
|
|
5329
|
+
// above ran in this commit, it read the ref just before this does, and a
|
|
5330
|
+
// mismatch means the ref changed in a render it didn't run for. The render
|
|
5331
|
+
// a bump asks for runs it, so the two match and the trap settles. An empty
|
|
5332
|
+
// ref matches a trap holding nothing, so a container that never mounts
|
|
5333
|
+
// costs no render.
|
|
5334
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
|
|
5335
|
+
useEffect(() => {
|
|
5336
|
+
if (active && hydrated && containerRef.current !== heldRef.current) {
|
|
5337
|
+
setRefChanges(n => n + 1);
|
|
5338
|
+
}
|
|
5339
|
+
});
|
|
5306
5340
|
}
|
|
5307
5341
|
|
|
5308
5342
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
@@ -8735,6 +8769,56 @@ const useInsideControl = () => useContext(ControlContext);
|
|
|
8735
8769
|
const FieldProvider = FieldContext.Provider;
|
|
8736
8770
|
/** Provider for Control context — used internally by Control component. */
|
|
8737
8771
|
const ControlProvider = ControlContext.Provider;
|
|
8772
|
+
// Each Field-shaping prop, with what takes its place on a `Field` the caller
|
|
8773
|
+
// wraps around the `Control`. `message` needs nothing there, because the
|
|
8774
|
+
// wrapper still renders it inside `Field > Control`. The other Field-level
|
|
8775
|
+
// props (`labelSize`, `labelProps`, `messageColor`) change nothing unless a
|
|
8776
|
+
// `label` or `message` renders, so on their own they keep no `Field`.
|
|
8777
|
+
// `bare-control.test.tsx` holds this list to what each wrapper hands its
|
|
8778
|
+
// `Field` and to which props change its markup, so a new Field-level prop
|
|
8779
|
+
// fails there until it is sorted into one group or the other.
|
|
8780
|
+
const FIELD_SHAPING = [
|
|
8781
|
+
['label', 'label'],
|
|
8782
|
+
['message', undefined],
|
|
8783
|
+
['horizontal', 'horizontal'],
|
|
8784
|
+
['fieldClassName', 'className (for fieldClassName)'],
|
|
8785
|
+
];
|
|
8786
|
+
/** `a`, `a and b`, `a, b and c`. */
|
|
8787
|
+
const listOf = (items) => items.length > 1
|
|
8788
|
+
? `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`
|
|
8789
|
+
: items[0];
|
|
8790
|
+
/**
|
|
8791
|
+
* Whether a convenience form wrapper renders a `Field` of its own (#905).
|
|
8792
|
+
* Never inside an outer `Field`. Inside a `Control` with no `Field` around
|
|
8793
|
+
* it, a `Field` of its own would put a `.field` inside the `.control`, where
|
|
8794
|
+
* Bulma's control styles break (its icon rules are sibling selectors on the
|
|
8795
|
+
* input). So it renders one there only when a prop that shapes the `Field`'s
|
|
8796
|
+
* markup is set (`label`, `message`, `horizontal`, `fieldClassName`), keeping
|
|
8797
|
+
* the markup it always had, and warns in development that the fix is a
|
|
8798
|
+
* `Field` around that `Control`. A prop counts when it is truthy, the same
|
|
8799
|
+
* test that decides whether it changes the `Field`'s output.
|
|
8800
|
+
* Internal; not part of the public API.
|
|
8801
|
+
*/
|
|
8802
|
+
const rendersOwnField = (component, options) => {
|
|
8803
|
+
if (options.insideField)
|
|
8804
|
+
return false;
|
|
8805
|
+
if (!options.insideControl)
|
|
8806
|
+
return true;
|
|
8807
|
+
const held = FIELD_SHAPING.filter(([prop]) => options[prop]);
|
|
8808
|
+
if (held.length === 0)
|
|
8809
|
+
return false;
|
|
8810
|
+
const names = held.map(([prop]) => prop);
|
|
8811
|
+
const moved = held
|
|
8812
|
+
.map(([, onField]) => onField)
|
|
8813
|
+
.filter((onField) => onField !== undefined);
|
|
8814
|
+
warnOnce(`${component}:Field-in-bare-Control:${names.join('+')}`, `[bestax-bulma] <${component} ${names.join(' ')}> inside a <Control> ` +
|
|
8815
|
+
`with no <Field> around it renders its own <Field> for ` +
|
|
8816
|
+
`${names.length > 1 ? 'those props' : 'that prop'}, which puts a ` +
|
|
8817
|
+
`.field inside the .control, a nesting Bulma's styles are not ` +
|
|
8818
|
+
`written for. Wrap the <Control> in a <Field> instead` +
|
|
8819
|
+
(moved.length > 0 ? `, and set ${listOf(moved)} on that <Field>.` : '.'));
|
|
8820
|
+
return true;
|
|
8821
|
+
};
|
|
8738
8822
|
const FieldLabelIdContext = createContext(undefined);
|
|
8739
8823
|
/**
|
|
8740
8824
|
* The id a labeled Field wants its single composed control to adopt (#495).
|
|
@@ -8865,6 +8949,18 @@ const Checkbox = forwardRef(({ color, size, className, children, textColor, disa
|
|
|
8865
8949
|
});
|
|
8866
8950
|
Checkbox.displayName = 'Checkbox';
|
|
8867
8951
|
|
|
8952
|
+
// Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
|
|
8953
|
+
// picker bases read it so their right-side launcher gives way to that
|
|
8954
|
+
// spinner, which sits at the same edge, whether the `Control` is the one a
|
|
8955
|
+
// convenience input renders or one the caller wrapped around the input.
|
|
8956
|
+
// Kept apart from `FormContext`, whose contexts only report whether a `Field`
|
|
8957
|
+
// or `Control` is present. Internal; not part of the public API.
|
|
8958
|
+
const ControlLoadingContext = createContext(false);
|
|
8959
|
+
/** Provider for the loading state, used internally by `Control`. */
|
|
8960
|
+
const ControlLoadingProvider = ControlLoadingContext.Provider;
|
|
8961
|
+
/** Whether the nearest enclosing `Control` is loading; `false` outside one. */
|
|
8962
|
+
const useControlLoading = () => useContext(ControlLoadingContext);
|
|
8963
|
+
|
|
8868
8964
|
const allowedColors = [...validColors, 'inherit', 'current'];
|
|
8869
8965
|
/**
|
|
8870
8966
|
* The `Control` component is a Bulma-styled wrapper for form controls (`Input`, `Select`, `TextArea`, etc.), supporting icons (left/right), loading state, expansion, size, and Bulma helper props for layout and color.
|
|
@@ -8924,7 +9020,7 @@ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isL
|
|
|
8924
9020
|
});
|
|
8925
9021
|
const controlClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
8926
9022
|
// --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
|
|
8927
|
-
return (jsx(ControlProvider, { value: true, children: jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [children, leftIconValue &&
|
|
9023
|
+
return (jsx(ControlProvider, { value: true, children: jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
|
|
8928
9024
|
(isIconProps(leftIconValue) ? (jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
|
|
8929
9025
|
(isIconProps(rightIconValue) ? (jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
|
|
8930
9026
|
});
|
|
@@ -9149,10 +9245,18 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, }) {
|
|
|
9149
9245
|
const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
|
|
9150
9246
|
const insideField = useInsideField();
|
|
9151
9247
|
const insideControl = useInsideControl();
|
|
9248
|
+
const ownField = rendersOwnField('Checkboxes', {
|
|
9249
|
+
insideField,
|
|
9250
|
+
insideControl,
|
|
9251
|
+
label,
|
|
9252
|
+
message,
|
|
9253
|
+
horizontal,
|
|
9254
|
+
fieldClassName,
|
|
9255
|
+
});
|
|
9152
9256
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
9153
9257
|
label,
|
|
9154
9258
|
labelProps,
|
|
9155
|
-
rendersLabel:
|
|
9259
|
+
rendersLabel: ownField,
|
|
9156
9260
|
});
|
|
9157
9261
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
9158
9262
|
...props,
|
|
@@ -9181,7 +9285,7 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
|
|
|
9181
9285
|
if (!insideControl) {
|
|
9182
9286
|
content = jsx(Control, { children: content });
|
|
9183
9287
|
}
|
|
9184
|
-
if (
|
|
9288
|
+
if (ownField) {
|
|
9185
9289
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9186
9290
|
}
|
|
9187
9291
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -9200,11 +9304,20 @@ const File = forwardRef(({
|
|
|
9200
9304
|
// Field props
|
|
9201
9305
|
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
|
|
9202
9306
|
const insideField = useInsideField();
|
|
9307
|
+
const insideControl = useInsideControl();
|
|
9308
|
+
const ownField = rendersOwnField('File', {
|
|
9309
|
+
insideField,
|
|
9310
|
+
insideControl,
|
|
9311
|
+
label,
|
|
9312
|
+
message,
|
|
9313
|
+
horizontal,
|
|
9314
|
+
fieldClassName,
|
|
9315
|
+
});
|
|
9203
9316
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
9204
9317
|
label,
|
|
9205
9318
|
id: props.id,
|
|
9206
9319
|
labelProps,
|
|
9207
|
-
rendersLabel:
|
|
9320
|
+
rendersLabel: ownField,
|
|
9208
9321
|
});
|
|
9209
9322
|
const { classPrefix } = useConfig();
|
|
9210
9323
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
@@ -9236,7 +9349,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
9236
9349
|
});
|
|
9237
9350
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
9238
9351
|
const fileElement = (jsx("div", { className: fileClass, children: jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, ...rest }), jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && fileName && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: fileName }))] }) }));
|
|
9239
|
-
if (
|
|
9352
|
+
if (ownField) {
|
|
9240
9353
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
|
|
9241
9354
|
}
|
|
9242
9355
|
return (jsxs(Fragment, { children: [fileElement, messageEl] }));
|
|
@@ -9369,10 +9482,18 @@ Radio.displayName = 'Radio';
|
|
|
9369
9482
|
const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
|
|
9370
9483
|
const insideField = useInsideField();
|
|
9371
9484
|
const insideControl = useInsideControl();
|
|
9485
|
+
const ownField = rendersOwnField('Radios', {
|
|
9486
|
+
insideField,
|
|
9487
|
+
insideControl,
|
|
9488
|
+
label,
|
|
9489
|
+
message,
|
|
9490
|
+
horizontal,
|
|
9491
|
+
fieldClassName,
|
|
9492
|
+
});
|
|
9372
9493
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
9373
9494
|
label,
|
|
9374
9495
|
labelProps,
|
|
9375
|
-
rendersLabel:
|
|
9496
|
+
rendersLabel: ownField,
|
|
9376
9497
|
});
|
|
9377
9498
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
9378
9499
|
...props,
|
|
@@ -9404,7 +9525,7 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
|
|
|
9404
9525
|
if (!insideControl) {
|
|
9405
9526
|
content = jsx(Control, { children: content });
|
|
9406
9527
|
}
|
|
9407
|
-
if (
|
|
9528
|
+
if (ownField) {
|
|
9408
9529
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9409
9530
|
}
|
|
9410
9531
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -9584,6 +9705,14 @@ const Slider = forwardRef((props, ref) => {
|
|
|
9584
9705
|
range: _range, value: _value, defaultValue: _defaultValue, onChange: _onChange, ariaLabel: _ariaLabel, minDistance: _minDistance, nameLow: _nameLow, nameHigh: _nameHigh, ...restProps } = props;
|
|
9585
9706
|
const insideField = useInsideField();
|
|
9586
9707
|
const insideControl = useInsideControl();
|
|
9708
|
+
const ownField = rendersOwnField('Slider', {
|
|
9709
|
+
insideField,
|
|
9710
|
+
insideControl,
|
|
9711
|
+
label,
|
|
9712
|
+
message,
|
|
9713
|
+
horizontal,
|
|
9714
|
+
fieldClassName,
|
|
9715
|
+
});
|
|
9587
9716
|
const { bulmaHelperClasses, rest } = useBulmaClasses(restProps);
|
|
9588
9717
|
// In range mode the low thumb carries `rest` (and any user id), so the
|
|
9589
9718
|
// label targets it; each thumb keeps its own aria-label for AT.
|
|
@@ -9591,7 +9720,7 @@ const Slider = forwardRef((props, ref) => {
|
|
|
9591
9720
|
label,
|
|
9592
9721
|
id: restProps.id,
|
|
9593
9722
|
labelProps,
|
|
9594
|
-
rendersLabel:
|
|
9723
|
+
rendersLabel: ownField,
|
|
9595
9724
|
});
|
|
9596
9725
|
// Resolve tooltip mode: explicit tooltip prop takes precedence, else showOutput maps to 'auto'
|
|
9597
9726
|
const tooltipMode = tooltip ?? (showOutput ? 'auto' : 'hidden');
|
|
@@ -9930,7 +10059,7 @@ const Slider = forwardRef((props, ref) => {
|
|
|
9930
10059
|
if (!insideControl) {
|
|
9931
10060
|
content = jsx(Control, { children: content });
|
|
9932
10061
|
}
|
|
9933
|
-
if (
|
|
10062
|
+
if (ownField) {
|
|
9934
10063
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9935
10064
|
}
|
|
9936
10065
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -10160,10 +10289,18 @@ function getFillPercent(iconIndex, value) {
|
|
|
10160
10289
|
const Rate = forwardRef(({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = 0, max = 5, size, disabled = false, showScore = false, showText = false, texts, onChange, customIcon, spaced = false, rtl = false, iconName, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, precision = 1, customText, name, form, className, ...props }, ref) => {
|
|
10161
10290
|
const insideField = useInsideField();
|
|
10162
10291
|
const insideControl = useInsideControl();
|
|
10292
|
+
const ownField = rendersOwnField('Rate', {
|
|
10293
|
+
insideField,
|
|
10294
|
+
insideControl,
|
|
10295
|
+
label,
|
|
10296
|
+
message,
|
|
10297
|
+
horizontal,
|
|
10298
|
+
fieldClassName,
|
|
10299
|
+
});
|
|
10163
10300
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
10164
10301
|
label,
|
|
10165
10302
|
labelProps,
|
|
10166
|
-
rendersLabel:
|
|
10303
|
+
rendersLabel: ownField,
|
|
10167
10304
|
});
|
|
10168
10305
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
10169
10306
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
@@ -10366,7 +10503,7 @@ const Rate = forwardRef(({ label, labelSize, labelProps, horizontal, message, me
|
|
|
10366
10503
|
if (!insideControl) {
|
|
10367
10504
|
content = jsx(Control, { children: content });
|
|
10368
10505
|
}
|
|
10369
|
-
if (
|
|
10506
|
+
if (ownField) {
|
|
10370
10507
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
10371
10508
|
}
|
|
10372
10509
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -10647,6 +10784,15 @@ const Taginput = forwardRef(({
|
|
|
10647
10784
|
// Field props
|
|
10648
10785
|
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = [], data = [], placeholder, field = 'label', allowNew = true, allowDuplicates = false, openOnFocus = false, removeOnKeys = true, confirmKeys = ['Enter', ','], closable = true, attached = false, maxTags, maxlength, disabled = false, readonly = false, rounded = false, ellipsis = false, hasCounter = true, onPasteSeparators = [','], beforeAdding, createTag, keepFirst = false, keepOpen = true, loading = false, ariaCloseLabel, icon, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, tagColor, size, onChange, onAdd, onRemove, onTyping, tagTemplate, name, form, id, className, ...props }, ref) => {
|
|
10649
10786
|
const insideField = useInsideField();
|
|
10787
|
+
const insideControl = useInsideControl();
|
|
10788
|
+
const ownField = rendersOwnField('Taginput', {
|
|
10789
|
+
insideField,
|
|
10790
|
+
insideControl,
|
|
10791
|
+
label,
|
|
10792
|
+
message,
|
|
10793
|
+
horizontal,
|
|
10794
|
+
fieldClassName,
|
|
10795
|
+
});
|
|
10650
10796
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
10651
10797
|
const { classPrefix } = useConfig();
|
|
10652
10798
|
const defaultIconLibrary = useIconLibrary();
|
|
@@ -10668,7 +10814,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
10668
10814
|
label,
|
|
10669
10815
|
id,
|
|
10670
10816
|
labelProps,
|
|
10671
|
-
rendersLabel:
|
|
10817
|
+
rendersLabel: ownField && !isMaxReached,
|
|
10672
10818
|
});
|
|
10673
10819
|
// Get display value from tag
|
|
10674
10820
|
const getDisplayValue = (tag) => {
|
|
@@ -10940,7 +11086,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
10940
11086
|
'is-active': index === highlightedIndex,
|
|
10941
11087
|
}), onClick: () => addTag(item), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": index === highlightedIndex, children: item }, index))) }) })), showCounter && jsx("small", { className: counterClasses, children: counterText }), name &&
|
|
10942
11088
|
tags.map((tag, i) => (jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
|
|
10943
|
-
if (
|
|
11089
|
+
if (ownField) {
|
|
10944
11090
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
|
|
10945
11091
|
}
|
|
10946
11092
|
return (jsxs(Fragment, { children: [taginputElement, messageEl] }));
|
|
@@ -11093,11 +11239,19 @@ fieldClassName, controlClassName,
|
|
|
11093
11239
|
...inputProps }, ref) => {
|
|
11094
11240
|
const insideField = useInsideField();
|
|
11095
11241
|
const insideControl = useInsideControl();
|
|
11242
|
+
const ownField = rendersOwnField('Input', {
|
|
11243
|
+
insideField,
|
|
11244
|
+
insideControl,
|
|
11245
|
+
label,
|
|
11246
|
+
message,
|
|
11247
|
+
horizontal,
|
|
11248
|
+
fieldClassName,
|
|
11249
|
+
});
|
|
11096
11250
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11097
11251
|
label,
|
|
11098
11252
|
id: inputProps.id,
|
|
11099
11253
|
labelProps,
|
|
11100
|
-
rendersLabel:
|
|
11254
|
+
rendersLabel: ownField,
|
|
11101
11255
|
});
|
|
11102
11256
|
const helpClass = usePrefixedClassNames('help', {
|
|
11103
11257
|
[`is-${messageColor}`]: !!messageColor,
|
|
@@ -11107,7 +11261,7 @@ fieldClassName, controlClassName,
|
|
|
11107
11261
|
content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
11108
11262
|
}
|
|
11109
11263
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
11110
|
-
if (
|
|
11264
|
+
if (ownField) {
|
|
11111
11265
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11112
11266
|
}
|
|
11113
11267
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -11143,11 +11297,19 @@ fieldClassName, controlClassName,
|
|
|
11143
11297
|
...selectProps }, ref) => {
|
|
11144
11298
|
const insideField = useInsideField();
|
|
11145
11299
|
const insideControl = useInsideControl();
|
|
11300
|
+
const ownField = rendersOwnField('Select', {
|
|
11301
|
+
insideField,
|
|
11302
|
+
insideControl,
|
|
11303
|
+
label,
|
|
11304
|
+
message,
|
|
11305
|
+
horizontal,
|
|
11306
|
+
fieldClassName,
|
|
11307
|
+
});
|
|
11146
11308
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11147
11309
|
label,
|
|
11148
11310
|
id: selectProps.id,
|
|
11149
11311
|
labelProps,
|
|
11150
|
-
rendersLabel:
|
|
11312
|
+
rendersLabel: ownField,
|
|
11151
11313
|
});
|
|
11152
11314
|
const helpClass = usePrefixedClassNames('help', {
|
|
11153
11315
|
[`is-${messageColor}`]: !!messageColor,
|
|
@@ -11157,7 +11319,7 @@ fieldClassName, controlClassName,
|
|
|
11157
11319
|
content = (jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
11158
11320
|
}
|
|
11159
11321
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
11160
|
-
if (
|
|
11322
|
+
if (ownField) {
|
|
11161
11323
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11162
11324
|
}
|
|
11163
11325
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -11194,11 +11356,19 @@ fieldClassName, controlClassName,
|
|
|
11194
11356
|
...textAreaProps }, ref) => {
|
|
11195
11357
|
const insideField = useInsideField();
|
|
11196
11358
|
const insideControl = useInsideControl();
|
|
11359
|
+
const ownField = rendersOwnField('TextArea', {
|
|
11360
|
+
insideField,
|
|
11361
|
+
insideControl,
|
|
11362
|
+
label,
|
|
11363
|
+
message,
|
|
11364
|
+
horizontal,
|
|
11365
|
+
fieldClassName,
|
|
11366
|
+
});
|
|
11197
11367
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11198
11368
|
label,
|
|
11199
11369
|
id: textAreaProps.id,
|
|
11200
11370
|
labelProps,
|
|
11201
|
-
rendersLabel:
|
|
11371
|
+
rendersLabel: ownField,
|
|
11202
11372
|
});
|
|
11203
11373
|
const helpClass = usePrefixedClassNames('help', {
|
|
11204
11374
|
[`is-${messageColor}`]: !!messageColor,
|
|
@@ -11208,7 +11378,7 @@ fieldClassName, controlClassName,
|
|
|
11208
11378
|
content = (jsx(Control, { isLoading: controlIsLoading, size: controlSize, className: controlClassName, children: content }));
|
|
11209
11379
|
}
|
|
11210
11380
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
11211
|
-
if (
|
|
11381
|
+
if (ownField) {
|
|
11212
11382
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11213
11383
|
}
|
|
11214
11384
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -11295,6 +11465,21 @@ function makeDate(year, month = 0, day = 1) {
|
|
|
11295
11465
|
d.setFullYear(year, month, day);
|
|
11296
11466
|
return d;
|
|
11297
11467
|
}
|
|
11468
|
+
/**
|
|
11469
|
+
* The earliest year the pickers accept. HTML's date, month and datetime-local
|
|
11470
|
+
* inputs hold no year below 1, so the calendar, typing and parsing stop there
|
|
11471
|
+
* too.
|
|
11472
|
+
*/
|
|
11473
|
+
const FIRST_YEAR = 1;
|
|
11474
|
+
/**
|
|
11475
|
+
* The lower bound a picker applies: `min`, raised to local midnight on
|
|
11476
|
+
* 1 January of {@link FIRST_YEAR} when it is earlier or absent. A `min` in
|
|
11477
|
+
* range comes back as the same object.
|
|
11478
|
+
*/
|
|
11479
|
+
function floorMin(min) {
|
|
11480
|
+
const floor = makeDate(FIRST_YEAR);
|
|
11481
|
+
return min && min.getTime() >= floor.getTime() ? min : floor;
|
|
11482
|
+
}
|
|
11298
11483
|
/**
|
|
11299
11484
|
* First instant of the day, month or year containing `d`. Works by setters on
|
|
11300
11485
|
* a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
|
|
@@ -11388,12 +11573,14 @@ function setTimeOfDay(d, parts) {
|
|
|
11388
11573
|
r.setSeconds(parts.seconds);
|
|
11389
11574
|
return r;
|
|
11390
11575
|
}
|
|
11576
|
+
/**
|
|
11577
|
+
* A copy of `d` moved into `[min, max]`. When `min` is after `max` nothing is
|
|
11578
|
+
* in range and `min` wins, so clamping twice gives the same date and a
|
|
11579
|
+
* picker's focus stays off the years before 1 under a `max` before then.
|
|
11580
|
+
*/
|
|
11391
11581
|
function clampDate(d, min, max) {
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
if (max && isAfter(d, max))
|
|
11395
|
-
return new Date(max);
|
|
11396
|
-
return new Date(d);
|
|
11582
|
+
const capped = max && isAfter(d, max) ? max : d;
|
|
11583
|
+
return new Date(min && isBefore(capped, min) ? min : capped);
|
|
11397
11584
|
}
|
|
11398
11585
|
/**
|
|
11399
11586
|
* Snap a Date's time-of-day to the nearest grid defined by the given
|
|
@@ -11466,7 +11653,9 @@ function formatToken(d, token) {
|
|
|
11466
11653
|
case 'YYYY':
|
|
11467
11654
|
// Zero-pad to a fixed 4 chars so the rendered width matches the
|
|
11468
11655
|
// segment-map width during digit-by-digit keyboard entry (and to match
|
|
11469
|
-
// ISO 8601)
|
|
11656
|
+
// ISO 8601), so year 19 shows as `0019`. Years >9999 keep their natural
|
|
11657
|
+
// width. The pickers commit no year below 1, so a minus sign reaches
|
|
11658
|
+
// this only from a `value` passed in.
|
|
11470
11659
|
return pad(d.getFullYear(), 4);
|
|
11471
11660
|
case 'YY':
|
|
11472
11661
|
return pad(d.getFullYear() % 100);
|
|
@@ -11740,9 +11929,10 @@ function getMonthNames(locale, length = 'long') {
|
|
|
11740
11929
|
|
|
11741
11930
|
/**
|
|
11742
11931
|
* Selects the calendar's focused cell, the one tab stop of whichever grid or
|
|
11743
|
-
* year list is on show, for a popover to put focus on as it opens.
|
|
11932
|
+
* year list is on show, for a popover to put focus on as it opens. It runs
|
|
11933
|
+
* against the whole panel, so it matches only inside the grid or the list.
|
|
11744
11934
|
*/
|
|
11745
|
-
const CALENDAR_FOCUSED_CELL = '[data-focused="true"], [data-focused-year="true"]';
|
|
11935
|
+
const CALENDAR_FOCUSED_CELL = '[role="grid"] [data-focused="true"], [role="listbox"] [data-focused-year="true"]';
|
|
11746
11936
|
const BASE_VIEW = {
|
|
11747
11937
|
day: 'days',
|
|
11748
11938
|
month: 'months',
|
|
@@ -11839,7 +12029,7 @@ function yearStep(key, focusedYear, years) {
|
|
|
11839
12029
|
return null;
|
|
11840
12030
|
}
|
|
11841
12031
|
}
|
|
11842
|
-
const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', }) => {
|
|
12032
|
+
const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minProp, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', }) => {
|
|
11843
12033
|
const gridRef = useRef(null);
|
|
11844
12034
|
const monthGridRef = useRef(null);
|
|
11845
12035
|
const yearGridRef = useRef(null);
|
|
@@ -11849,6 +12039,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11849
12039
|
const isDayGranularity = granularity === 'day';
|
|
11850
12040
|
const isYearGranularity = granularity === 'year';
|
|
11851
12041
|
const [view, setView] = useState(baseView);
|
|
12042
|
+
// Nothing before year 1 can be picked or reached, as in HTML's date inputs.
|
|
12043
|
+
const min = useMemo(() => floorMin(minProp), [minProp]);
|
|
11852
12044
|
const computedDayNames = useMemo(() => {
|
|
11853
12045
|
if (dayNames && dayNames.length === 7)
|
|
11854
12046
|
return dayNames;
|
|
@@ -11919,7 +12111,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11919
12111
|
const moveFocus = useCallback((next, direction) => {
|
|
11920
12112
|
let candidate = next;
|
|
11921
12113
|
for (let i = 0; i < 366; i++) {
|
|
11922
|
-
if (
|
|
12114
|
+
if (candidate.getTime() < startOfDay(min).getTime())
|
|
11923
12115
|
return;
|
|
11924
12116
|
if (max && candidate.getTime() > startOfDay(max).getTime())
|
|
11925
12117
|
return;
|
|
@@ -11985,7 +12177,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11985
12177
|
let candidate = next;
|
|
11986
12178
|
for (let i = 0; i < 120; i++) {
|
|
11987
12179
|
const month = startOfMonth(candidate).getTime();
|
|
11988
|
-
if (
|
|
12180
|
+
if (month < startOfMonth(min).getTime())
|
|
11989
12181
|
return;
|
|
11990
12182
|
if (max && month > startOfMonth(max).getTime())
|
|
11991
12183
|
return;
|
|
@@ -12033,25 +12225,22 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12033
12225
|
const today = new Date();
|
|
12034
12226
|
const todayYear = today.getFullYear();
|
|
12035
12227
|
// The header steps a month at a time over the day grid and a year at a
|
|
12036
|
-
// time over the month grid, kept inside min/max.
|
|
12037
|
-
|
|
12038
|
-
|
|
12228
|
+
// time over the month grid, kept inside min/max. A step is off once the
|
|
12229
|
+
// month or year on show holds the bound or lies past it, as picking a year
|
|
12230
|
+
// from the list can leave it.
|
|
12039
12231
|
const prevAnchor = isDayGranularity
|
|
12040
|
-
?
|
|
12232
|
+
? addMonths(focusedDate, -1)
|
|
12041
12233
|
: clampDate(addYears(focusedDate, -1), min, max);
|
|
12042
12234
|
const nextAnchor = isDayGranularity
|
|
12043
|
-
?
|
|
12235
|
+
? addMonths(focusedDate, 1)
|
|
12044
12236
|
: clampDate(addYears(focusedDate, 1), min, max);
|
|
12237
|
+
const focusedMonth = startOfMonth(focusedDate).getTime();
|
|
12045
12238
|
const prevDisabled = isDayGranularity
|
|
12046
|
-
?
|
|
12047
|
-
|
|
12048
|
-
isSameMonth(focusedDate, min))
|
|
12049
|
-
: !!(min && focusedYear <= min.getFullYear());
|
|
12239
|
+
? focusedMonth <= startOfMonth(min).getTime()
|
|
12240
|
+
: focusedYear <= min.getFullYear();
|
|
12050
12241
|
const nextDisabled = isDayGranularity
|
|
12051
|
-
? !!
|
|
12052
|
-
|
|
12053
|
-
isSameMonth(focusedDate, max))
|
|
12054
|
-
: !!(max && focusedYear >= max.getFullYear());
|
|
12242
|
+
? !!max && focusedMonth >= startOfMonth(max).getTime()
|
|
12243
|
+
: !!max && focusedYear >= max.getFullYear();
|
|
12055
12244
|
const labelId = id ? `${id}-label` : undefined;
|
|
12056
12245
|
const monthLabel = isDayGranularity
|
|
12057
12246
|
? `${computedMonthNames[focusedDate.getMonth()]} ${focusedYear}`
|
|
@@ -12062,7 +12251,9 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12062
12251
|
const [mountYear] = useState(focusedYear);
|
|
12063
12252
|
const windowYear = isYearGranularity ? mountYear : focusedYear;
|
|
12064
12253
|
const yearList = useMemo(() => {
|
|
12065
|
-
const minYear =
|
|
12254
|
+
const minYear = minProp
|
|
12255
|
+
? min.getFullYear()
|
|
12256
|
+
: Math.max(FIRST_YEAR, windowYear - 100);
|
|
12066
12257
|
const maxYear = max ? max.getFullYear() : windowYear + 100;
|
|
12067
12258
|
const [lo, hi] = yearsRange ?? [minYear, maxYear];
|
|
12068
12259
|
const start = Math.max(lo, minYear);
|
|
@@ -12071,20 +12262,25 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12071
12262
|
for (let y = start; y <= end; y++)
|
|
12072
12263
|
out.push(y);
|
|
12073
12264
|
return out;
|
|
12074
|
-
}, [windowYear, min, max, yearsRange]);
|
|
12265
|
+
}, [windowYear, minProp, min, max, yearsRange]);
|
|
12075
12266
|
// Only the selection surface disables years; as navigation every listed
|
|
12076
12267
|
// year can be visited.
|
|
12077
12268
|
const disabledYears = useMemo(() => new Set(isYearGranularity ? yearList.filter(y => isYearUnselectable(y)) : []), [isYearGranularity, yearList, isYearUnselectable]);
|
|
12078
12269
|
// The year list's one tab stop, after the month grid's: when the focused
|
|
12079
12270
|
// year is disabled the stop moves to the nearest enabled year in the list,
|
|
12080
|
-
// and focusing it makes it the focused year.
|
|
12081
|
-
// the
|
|
12271
|
+
// and focusing it makes it the focused year. As the selection surface a
|
|
12272
|
+
// year outside a non-empty list starts from the end it fell past, so the
|
|
12273
|
+
// list keeps a stop. As navigation the stop marks the current year, so a
|
|
12274
|
+
// year outside the list marks no option.
|
|
12082
12275
|
const tabStopYear = useMemo(() => {
|
|
12083
|
-
|
|
12084
|
-
if (i < 0)
|
|
12085
|
-
|
|
12276
|
+
let i = yearList.indexOf(focusedYear);
|
|
12277
|
+
if (i < 0) {
|
|
12278
|
+
if (!isYearGranularity || yearList.length === 0)
|
|
12279
|
+
return focusedYear;
|
|
12280
|
+
i = focusedYear < yearList[0] ? 0 : yearList.length - 1;
|
|
12281
|
+
}
|
|
12086
12282
|
return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
|
|
12087
|
-
}, [yearList, focusedYear, disabledYears]);
|
|
12283
|
+
}, [yearList, focusedYear, disabledYears, isYearGranularity]);
|
|
12088
12284
|
// Which of the focused year's months have no selectable day.
|
|
12089
12285
|
const disabledMonths = useMemo(() => granularity === 'month'
|
|
12090
12286
|
? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
|
|
@@ -12291,11 +12487,10 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
|
|
|
12291
12487
|
active: isOpen,
|
|
12292
12488
|
position,
|
|
12293
12489
|
fixed: appendToBody,
|
|
12294
|
-
//
|
|
12295
|
-
//
|
|
12296
|
-
// the panel.
|
|
12297
|
-
|
|
12298
|
-
offset: appendToBody ? 0 : undefined,
|
|
12490
|
+
// The gap comes from `--bulma-picker-popover-offset` as a margin, in place
|
|
12491
|
+
// or portaled, so the coordinates leave it out and `auto` reads it off
|
|
12492
|
+
// the panel.
|
|
12493
|
+
offset: 0,
|
|
12299
12494
|
styledGap: marginGap,
|
|
12300
12495
|
});
|
|
12301
12496
|
useEffect(() => {
|
|
@@ -12741,33 +12936,35 @@ function useSegmentedEntry(params) {
|
|
|
12741
12936
|
// within the same event tick.
|
|
12742
12937
|
const typedDigitsRef = useRef('');
|
|
12743
12938
|
// The text focus seeded an empty picker with, until the user edits it or
|
|
12744
|
-
// leaves.
|
|
12939
|
+
// leaves.
|
|
12745
12940
|
const seedRef = useRef(null);
|
|
12746
|
-
// Whether the
|
|
12747
|
-
//
|
|
12748
|
-
|
|
12941
|
+
// Whether the text is still what the picker put there, so nothing in it was
|
|
12942
|
+
// typed: the value's display, or an empty picker's seed until an edit drops
|
|
12943
|
+
// it. Against a value it compares text alone, so typing that ends back on
|
|
12944
|
+
// the display counts as untouched.
|
|
12945
|
+
const isUntouched = useCallback((s) => value
|
|
12946
|
+
? s === formatFn(value, format, locale)
|
|
12947
|
+
: seedRef.current !== null && s === seedRef.current, [value, formatFn, format, locale]);
|
|
12749
12948
|
// A seed the popover never turned into a value closes with it, wherever
|
|
12750
12949
|
// focus goes, so a dismissed empty field stays empty.
|
|
12751
12950
|
//
|
|
12752
12951
|
// As the popover closes, its focus trap hands focus back to the input. That
|
|
12753
12952
|
// focus is not the user arriving: it must not open the popover again under
|
|
12754
|
-
// `openOnFocus`, or seed an empty picker
|
|
12755
|
-
//
|
|
12756
|
-
//
|
|
12757
|
-
//
|
|
12758
|
-
// in between is the one handed back.
|
|
12953
|
+
// `openOnFocus`, or seed an empty picker. `closingRef` is set in the layout
|
|
12954
|
+
// phase of the commit that closes it and cleared by that commit's effects,
|
|
12955
|
+
// and React runs a commit's effect cleanups, the trap's among them, before
|
|
12956
|
+
// any of its effects, so a focus in between is the one handed back.
|
|
12759
12957
|
const closingRef = useRef(false);
|
|
12760
12958
|
const wasOpenRef = useRef(isOpen);
|
|
12761
12959
|
useLayoutEffect(() => {
|
|
12762
12960
|
if (wasOpenRef.current && !isOpen) {
|
|
12763
12961
|
closingRef.current = true;
|
|
12764
|
-
if (!value &&
|
|
12962
|
+
if (!value && isUntouched(text))
|
|
12765
12963
|
setText('');
|
|
12766
|
-
}
|
|
12767
12964
|
seedRef.current = null;
|
|
12768
12965
|
}
|
|
12769
12966
|
wasOpenRef.current = isOpen;
|
|
12770
|
-
}, [isOpen, value, text, setText]);
|
|
12967
|
+
}, [isOpen, value, isUntouched, text, setText]);
|
|
12771
12968
|
useEffect(() => {
|
|
12772
12969
|
closingRef.current = false;
|
|
12773
12970
|
}, [isOpen]);
|
|
@@ -12845,13 +13042,18 @@ function useSegmentedEntry(params) {
|
|
|
12845
13042
|
}
|
|
12846
13043
|
setActiveSegmentIdx(null);
|
|
12847
13044
|
typedDigitsRef.current = '';
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
//
|
|
12853
|
-
//
|
|
12854
|
-
|
|
13045
|
+
// Leaving commits only what was typed. Re-parsing the value's display
|
|
13046
|
+
// could drop what the format leaves out, such as seconds, and a seed
|
|
13047
|
+
// nobody typed over is not the user's date.
|
|
13048
|
+
const untouched = isUntouched(text);
|
|
13049
|
+
// An open popover keeps an unused seed until it closes, so one portaled
|
|
13050
|
+
// outside the picker, whose focus reads as leaving, keeps it as one
|
|
13051
|
+
// inside does.
|
|
13052
|
+
if (!isOpen)
|
|
13053
|
+
seedRef.current = null;
|
|
13054
|
+
if (untouched) {
|
|
13055
|
+
if (!value && !isOpen)
|
|
13056
|
+
setText('');
|
|
12855
13057
|
onBlur?.(e);
|
|
12856
13058
|
return;
|
|
12857
13059
|
}
|
|
@@ -12865,6 +13067,8 @@ function useSegmentedEntry(params) {
|
|
|
12865
13067
|
onBlur?.(e);
|
|
12866
13068
|
}, [
|
|
12867
13069
|
containerRef,
|
|
13070
|
+
isUntouched,
|
|
13071
|
+
isOpen,
|
|
12868
13072
|
tryParse,
|
|
12869
13073
|
text,
|
|
12870
13074
|
isAllowed,
|
|
@@ -12878,7 +13082,6 @@ function useSegmentedEntry(params) {
|
|
|
12878
13082
|
]);
|
|
12879
13083
|
const handleFocus = useCallback((e) => {
|
|
12880
13084
|
const handedBack = closingRef.current;
|
|
12881
|
-
handedBackRef.current = handedBack;
|
|
12882
13085
|
if (!handedBack && openOnFocus && popover && !disabled && !readOnly) {
|
|
12883
13086
|
setOpen(true);
|
|
12884
13087
|
}
|
|
@@ -13046,12 +13249,20 @@ function useSegmentedEntry(params) {
|
|
|
13046
13249
|
}
|
|
13047
13250
|
if (e.key === 'Enter') {
|
|
13048
13251
|
e.preventDefault();
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13252
|
+
// Untouched text has nothing to commit, as on leaving, but Enter
|
|
13253
|
+
// still confirms what the field shows.
|
|
13254
|
+
if (isUntouched(text)) {
|
|
13052
13255
|
if (closeOnSelect)
|
|
13053
13256
|
setOpen(false);
|
|
13054
13257
|
}
|
|
13258
|
+
else {
|
|
13259
|
+
const parsed = tryParse(text);
|
|
13260
|
+
if (parsed && isAllowed(parsed)) {
|
|
13261
|
+
commitValue(parsed);
|
|
13262
|
+
if (closeOnSelect)
|
|
13263
|
+
setOpen(false);
|
|
13264
|
+
}
|
|
13265
|
+
}
|
|
13055
13266
|
}
|
|
13056
13267
|
onKeyDown?.(e);
|
|
13057
13268
|
}, [
|
|
@@ -13065,6 +13276,7 @@ function useSegmentedEntry(params) {
|
|
|
13065
13276
|
setOpen,
|
|
13066
13277
|
closeOnSelect,
|
|
13067
13278
|
popover,
|
|
13279
|
+
isUntouched,
|
|
13068
13280
|
tryParse,
|
|
13069
13281
|
text,
|
|
13070
13282
|
isAllowed,
|
|
@@ -13113,9 +13325,12 @@ const pad2$1 = (n) => String(n).padStart(2, '0');
|
|
|
13113
13325
|
* The value as the native input and the hidden form input carry it:
|
|
13114
13326
|
* `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
|
|
13115
13327
|
* `<input type="month">` use. The year is padded to four digits, as those
|
|
13116
|
-
* inputs require and as the `YYYY` token displays it.
|
|
13328
|
+
* inputs require and as the `YYYY` token displays it. HTML has no such shape
|
|
13329
|
+
* for a year before 1, so a date then is empty, as those inputs would make it.
|
|
13117
13330
|
*/
|
|
13118
13331
|
const toIsoValue = (d, granularity) => {
|
|
13332
|
+
if (d.getFullYear() < FIRST_YEAR)
|
|
13333
|
+
return '';
|
|
13119
13334
|
const year = String(d.getFullYear()).padStart(4, '0');
|
|
13120
13335
|
if (granularity === 'year')
|
|
13121
13336
|
return year;
|
|
@@ -13156,14 +13371,19 @@ const SKIPPED_SEGMENTS = {
|
|
|
13156
13371
|
* @returns {JSX.Element}
|
|
13157
13372
|
*/
|
|
13158
13373
|
const DateInputBase = forwardRef((props, ref) => {
|
|
13159
|
-
const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, granularity = 'day', format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = true, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon
|
|
13374
|
+
const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, granularity = 'day', format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = true, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', labels, ...rest } = props;
|
|
13375
|
+
// The launcher gives way to the loading spinner of a Control this sits in.
|
|
13376
|
+
const controlLoading = useControlLoading();
|
|
13377
|
+
const triggerIcon = triggerIconProp ?? !controlLoading;
|
|
13160
13378
|
const t = mergeLabels(labels);
|
|
13161
13379
|
const isDayGranularity = granularity === 'day';
|
|
13162
13380
|
const resolvedFormat = format ?? DEFAULT_FORMATS[granularity];
|
|
13163
13381
|
const isControlled = controlledValue !== undefined;
|
|
13164
13382
|
const [internalValue, setInternalValue] = useState(defaultValue ?? null);
|
|
13165
13383
|
const value = isControlled ? (controlledValue ?? null) : internalValue;
|
|
13166
|
-
|
|
13384
|
+
// Nothing before year 1 is in range, as in HTML's date inputs.
|
|
13385
|
+
const lowerBound = useMemo(() => floorMin(min), [min]);
|
|
13386
|
+
const initialFocused = useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
|
|
13167
13387
|
// intentionally only on mount: keep focusedDate stable until value/open change
|
|
13168
13388
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
13169
13389
|
[]);
|
|
@@ -13171,8 +13391,8 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13171
13391
|
// Re-clamp focusedDate if min/max change after mount so the focused cell
|
|
13172
13392
|
// never disappears outside the displayable range.
|
|
13173
13393
|
useEffect(() => {
|
|
13174
|
-
setFocusedDate(prev => clampDate(prev,
|
|
13175
|
-
}, [
|
|
13394
|
+
setFocusedDate(prev => clampDate(prev, lowerBound, max));
|
|
13395
|
+
}, [lowerBound, max]);
|
|
13176
13396
|
const [open, setOpenState] = useState(false);
|
|
13177
13397
|
const [text, setText] = useState(value ? formatDate(value, resolvedFormat, locale) : '');
|
|
13178
13398
|
const inputRef = useRef(null);
|
|
@@ -13230,7 +13450,7 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13230
13450
|
}, [isControlled, onChange, isDayGranularity, granularity]);
|
|
13231
13451
|
// At month or year granularity the bounds widen to the periods holding
|
|
13232
13452
|
// them, so the month or year containing `min` stays reachable.
|
|
13233
|
-
const periodMin = useMemo(() =>
|
|
13453
|
+
const periodMin = useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
|
|
13234
13454
|
const periodMax = useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
|
|
13235
13455
|
const handleSelect = useCallback((d) => {
|
|
13236
13456
|
if (!isWithin(d, periodMin, periodMax))
|
|
@@ -13281,7 +13501,7 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13281
13501
|
]);
|
|
13282
13502
|
// Typing, and the re-parse on blur or Enter, that lands in the period the
|
|
13283
13503
|
// value already holds commits nothing, so a `value` elsewhere in its month
|
|
13284
|
-
// or year
|
|
13504
|
+
// or year changes only when the user picks or types a different period.
|
|
13285
13505
|
const commitTyped = useCallback((next) => {
|
|
13286
13506
|
if (next &&
|
|
13287
13507
|
value &&
|
|
@@ -13347,7 +13567,7 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13347
13567
|
? fromIsoValue(e.target.value, granularity)
|
|
13348
13568
|
: null;
|
|
13349
13569
|
commitValue(parsed);
|
|
13350
|
-
}, min: min ? toIsoValue(
|
|
13570
|
+
}, min: min ? toIsoValue(lowerBound, granularity) : undefined, max: max ? toIsoValue(max, granularity) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
|
|
13351
13571
|
}
|
|
13352
13572
|
const chooseLabel = granularity === 'month'
|
|
13353
13573
|
? t.chooseMonth
|
|
@@ -13356,7 +13576,11 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13356
13576
|
: t.chooseDate;
|
|
13357
13577
|
const calendar = (jsx(Calendar
|
|
13358
13578
|
// A new granularity starts the calendar over on its own view.
|
|
13359
|
-
, { granularity: granularity, value: value, focusedDate: focusedDate, onSelect: handleSelect, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size,
|
|
13579
|
+
, { granularity: granularity, value: value, focusedDate: focusedDate, onSelect: handleSelect, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size,
|
|
13580
|
+
// The popover panel takes `popoverId`, so the calendar inside it
|
|
13581
|
+
// takes one of its own. Inline there is no panel, and the calendar
|
|
13582
|
+
// keeps the id it released with.
|
|
13583
|
+
id: inline ? popoverId : `${popoverId}-cal`, autoFocusCell: open, labels: labels }, granularity));
|
|
13360
13584
|
if (inline) {
|
|
13361
13585
|
return (jsxs("div", { ...cleanRest, ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), children: [calendar, name && (jsx("input", { type: "hidden", name: name, form: form, value: value ? toIsoValue(value, granularity) : '', required: required }))] }));
|
|
13362
13586
|
}
|
|
@@ -13387,26 +13611,34 @@ DateInputBase.displayName = 'DateInputBase';
|
|
|
13387
13611
|
const DateInput = forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
|
|
13388
13612
|
const insideField = useInsideField();
|
|
13389
13613
|
const insideControl = useInsideControl();
|
|
13614
|
+
const ownField = rendersOwnField('DateInput', {
|
|
13615
|
+
insideField,
|
|
13616
|
+
insideControl,
|
|
13617
|
+
label,
|
|
13618
|
+
message,
|
|
13619
|
+
horizontal,
|
|
13620
|
+
fieldClassName,
|
|
13621
|
+
});
|
|
13390
13622
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
13391
13623
|
label,
|
|
13392
13624
|
id: baseProps.id,
|
|
13393
13625
|
labelProps,
|
|
13394
13626
|
// Inline mode renders a bare calendar with no input to label.
|
|
13395
|
-
rendersLabel:
|
|
13627
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
13396
13628
|
});
|
|
13397
13629
|
const helpClass = usePrefixedClassNames('help', {
|
|
13398
13630
|
[`is-${messageColor}`]: !!messageColor,
|
|
13399
13631
|
});
|
|
13400
|
-
// The
|
|
13401
|
-
//
|
|
13402
|
-
let content = (jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps
|
|
13632
|
+
// The base hides its launcher while the Control it sits in is loading,
|
|
13633
|
+
// whether that is the one rendered below or an enclosing one.
|
|
13634
|
+
let content = (jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps }));
|
|
13403
13635
|
// Inline mode renders a bare calendar with no input, so the Control's
|
|
13404
13636
|
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
13405
13637
|
if (!insideControl && !baseProps.inline) {
|
|
13406
13638
|
content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
13407
13639
|
}
|
|
13408
13640
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
13409
|
-
if (
|
|
13641
|
+
if (ownField) {
|
|
13410
13642
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
13411
13643
|
}
|
|
13412
13644
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -14280,7 +14512,10 @@ const fromIsoTime = (s) => {
|
|
|
14280
14512
|
* @returns {JSX.Element}
|
|
14281
14513
|
*/
|
|
14282
14514
|
const TimeInputBase = forwardRef((props, ref) => {
|
|
14283
|
-
const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, hourFormat = '24', enableSeconds = false, incrementHours = 1, incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon
|
|
14515
|
+
const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, hourFormat = '24', enableSeconds = false, incrementHours = 1, incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', labels, audioTick = false, haptics = false, ...rest } = props;
|
|
14516
|
+
// The launcher gives way to the loading spinner of a Control this sits in.
|
|
14517
|
+
const controlLoading = useControlLoading();
|
|
14518
|
+
const triggerIcon = triggerIconProp ?? !controlLoading;
|
|
14284
14519
|
// Platform-appropriate feedback routing: when `haptics` is opted in and
|
|
14285
14520
|
// the runtime has no Vibration API (notably iOS Safari), enable the
|
|
14286
14521
|
// audio thunk so the user still gets *some* tactile cue. When vibrate
|
|
@@ -14357,6 +14592,8 @@ const TimeInputBase = forwardRef((props, ref) => {
|
|
|
14357
14592
|
useEffect(() => {
|
|
14358
14593
|
setText(value ? formatTime(value, defaultFormat, locale) : '');
|
|
14359
14594
|
}, [value, defaultFormat, locale]);
|
|
14595
|
+
// A value's date before year 1 is out of range, as in the date pickers.
|
|
14596
|
+
const lowerBound = useMemo(() => floorMin(min), [min]);
|
|
14360
14597
|
const commitValue = useCallback((next) => {
|
|
14361
14598
|
if (!isControlled)
|
|
14362
14599
|
setInternalValue(next);
|
|
@@ -14365,10 +14602,10 @@ const TimeInputBase = forwardRef((props, ref) => {
|
|
|
14365
14602
|
const handleSpinnerChange = useCallback((parts) => {
|
|
14366
14603
|
const base = value ?? new Date();
|
|
14367
14604
|
const next = setTimeOfDay(base, parts);
|
|
14368
|
-
if (!isWithin(next,
|
|
14605
|
+
if (!isWithin(next, lowerBound, max))
|
|
14369
14606
|
return;
|
|
14370
14607
|
commitValue(next);
|
|
14371
|
-
}, [value,
|
|
14608
|
+
}, [value, lowerBound, max, commitValue]);
|
|
14372
14609
|
const tryParse = useCallback((s) => {
|
|
14373
14610
|
const trimmed = s.trim();
|
|
14374
14611
|
if (!trimmed)
|
|
@@ -14400,7 +14637,7 @@ const TimeInputBase = forwardRef((props, ref) => {
|
|
|
14400
14637
|
setText,
|
|
14401
14638
|
makeBaseDate,
|
|
14402
14639
|
locale,
|
|
14403
|
-
min,
|
|
14640
|
+
min: lowerBound,
|
|
14404
14641
|
max,
|
|
14405
14642
|
isBlocked: unselectableTimes,
|
|
14406
14643
|
disabled,
|
|
@@ -14482,34 +14719,46 @@ TimeInputBase.displayName = 'TimeInputBase';
|
|
|
14482
14719
|
const TimeInput = forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'clock', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
|
|
14483
14720
|
const insideField = useInsideField();
|
|
14484
14721
|
const insideControl = useInsideControl();
|
|
14722
|
+
const ownField = rendersOwnField('TimeInput', {
|
|
14723
|
+
insideField,
|
|
14724
|
+
insideControl,
|
|
14725
|
+
label,
|
|
14726
|
+
message,
|
|
14727
|
+
horizontal,
|
|
14728
|
+
fieldClassName,
|
|
14729
|
+
});
|
|
14485
14730
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
14486
14731
|
label,
|
|
14487
14732
|
id: baseProps.id,
|
|
14488
14733
|
labelProps,
|
|
14489
14734
|
// Inline mode renders a bare panel with no input to label.
|
|
14490
|
-
rendersLabel:
|
|
14735
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
14491
14736
|
});
|
|
14492
14737
|
const helpClass = usePrefixedClassNames('help', {
|
|
14493
14738
|
[`is-${messageColor}`]: !!messageColor,
|
|
14494
14739
|
});
|
|
14495
|
-
// The
|
|
14496
|
-
//
|
|
14497
|
-
let content = (jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps
|
|
14740
|
+
// The base hides its launcher while the Control it sits in is loading,
|
|
14741
|
+
// whether that is the one rendered below or an enclosing one.
|
|
14742
|
+
let content = (jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps }));
|
|
14498
14743
|
// Inline mode renders a bare panel with no input, so the Control's
|
|
14499
14744
|
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
14500
14745
|
if (!insideControl && !baseProps.inline) {
|
|
14501
14746
|
content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
14502
14747
|
}
|
|
14503
14748
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
14504
|
-
if (
|
|
14749
|
+
if (ownField) {
|
|
14505
14750
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
14506
14751
|
}
|
|
14507
14752
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
14508
14753
|
});
|
|
14509
14754
|
TimeInput.displayName = 'TimeInput';
|
|
14510
14755
|
|
|
14511
|
-
// The year is padded to four digits, as `datetime-local` requires.
|
|
14756
|
+
// The year is padded to four digits, as `datetime-local` requires. HTML has
|
|
14757
|
+
// no such shape for a year before 1, so a date then is empty, as that input
|
|
14758
|
+
// would make it.
|
|
14512
14759
|
const toIsoDateTime = (d, withSeconds) => {
|
|
14760
|
+
if (d.getFullYear() < FIRST_YEAR)
|
|
14761
|
+
return '';
|
|
14513
14762
|
const yyyy = String(d.getFullYear()).padStart(4, '0');
|
|
14514
14763
|
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
|
14515
14764
|
const dd = String(d.getDate()).padStart(2, '0');
|
|
@@ -14542,7 +14791,10 @@ const fromIsoDateTime = (s) => {
|
|
|
14542
14791
|
const DateTimeInputBase = forwardRef((props, ref) => {
|
|
14543
14792
|
const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, hourFormat = '24', enableSeconds = false, incrementHours = 1,
|
|
14544
14793
|
// Step between visible wheel values; 1 gives the iOS-style every-minute wheel.
|
|
14545
|
-
incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon
|
|
14794
|
+
incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', audioTick = false, haptics = false, labels, ...rest } = props;
|
|
14795
|
+
// The launcher gives way to the loading spinner of a Control this sits in.
|
|
14796
|
+
const controlLoading = useControlLoading();
|
|
14797
|
+
const triggerIcon = triggerIconProp ?? !controlLoading;
|
|
14546
14798
|
const t = mergeLabels(labels);
|
|
14547
14799
|
// Platform-appropriate feedback routing for the time wheel: on iOS (no
|
|
14548
14800
|
// navigator.vibrate) the audio thunk fills in; on Android the real haptic
|
|
@@ -14552,14 +14804,16 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14552
14804
|
const isControlled = controlledValue !== undefined;
|
|
14553
14805
|
const [internalValue, setInternalValue] = useState(defaultValue ?? null);
|
|
14554
14806
|
const value = isControlled ? (controlledValue ?? null) : internalValue;
|
|
14555
|
-
|
|
14807
|
+
// Nothing before year 1 is in range, as in HTML's date inputs.
|
|
14808
|
+
const lowerBound = useMemo(() => floorMin(min), [min]);
|
|
14809
|
+
const initialFocused = useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
|
|
14556
14810
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14557
14811
|
[]);
|
|
14558
14812
|
const [focusedDate, setFocusedDate] = useState(initialFocused);
|
|
14559
14813
|
// Re-clamp focusedDate when min/max change so the focused cell stays valid.
|
|
14560
14814
|
useEffect(() => {
|
|
14561
|
-
setFocusedDate(prev => clampDate(prev,
|
|
14562
|
-
}, [
|
|
14815
|
+
setFocusedDate(prev => clampDate(prev, lowerBound, max));
|
|
14816
|
+
}, [lowerBound, max]);
|
|
14563
14817
|
const defaultFormat = format ??
|
|
14564
14818
|
(hourFormat === '12'
|
|
14565
14819
|
? enableSeconds
|
|
@@ -14656,18 +14910,18 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14656
14910
|
minutes: value?.getMinutes() ?? 0,
|
|
14657
14911
|
seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
|
|
14658
14912
|
});
|
|
14659
|
-
if (!isWithin(merged,
|
|
14913
|
+
if (!isWithin(merged, lowerBound, max))
|
|
14660
14914
|
return;
|
|
14661
14915
|
commitValue(merged);
|
|
14662
14916
|
setFocusedDate(d);
|
|
14663
|
-
}, [value, enableSeconds,
|
|
14917
|
+
}, [value, enableSeconds, lowerBound, max, commitValue]);
|
|
14664
14918
|
const handleTimeChange = useCallback((parts) => {
|
|
14665
14919
|
const base = value ?? focusedDate;
|
|
14666
14920
|
const next = setTimeOfDay(base, parts);
|
|
14667
|
-
if (!isWithin(next,
|
|
14921
|
+
if (!isWithin(next, lowerBound, max))
|
|
14668
14922
|
return;
|
|
14669
14923
|
commitValue(next);
|
|
14670
|
-
}, [value, focusedDate,
|
|
14924
|
+
}, [value, focusedDate, lowerBound, max, commitValue]);
|
|
14671
14925
|
const tryParse = useCallback((s) => {
|
|
14672
14926
|
const trimmed = s.trim();
|
|
14673
14927
|
if (!trimmed)
|
|
@@ -14702,7 +14956,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14702
14956
|
setText,
|
|
14703
14957
|
makeBaseDate,
|
|
14704
14958
|
locale,
|
|
14705
|
-
min,
|
|
14959
|
+
min: lowerBound,
|
|
14706
14960
|
max,
|
|
14707
14961
|
isBlocked,
|
|
14708
14962
|
disabled,
|
|
@@ -14735,7 +14989,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14735
14989
|
? fromIsoDateTime(e.target.value)
|
|
14736
14990
|
: null;
|
|
14737
14991
|
commitValue(parsed);
|
|
14738
|
-
}, min: min ? toIsoDateTime(
|
|
14992
|
+
}, min: min ? toIsoDateTime(lowerBound, enableSeconds) : undefined, max: max ? toIsoDateTime(max, enableSeconds) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
|
|
14739
14993
|
}
|
|
14740
14994
|
const spinnerValue = {
|
|
14741
14995
|
hours: value?.getHours() ?? 0,
|
|
@@ -14786,26 +15040,34 @@ DateTimeInputBase.displayName = 'DateTimeInputBase';
|
|
|
14786
15040
|
const DateTimeInput = forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar-alt', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
|
|
14787
15041
|
const insideField = useInsideField();
|
|
14788
15042
|
const insideControl = useInsideControl();
|
|
15043
|
+
const ownField = rendersOwnField('DateTimeInput', {
|
|
15044
|
+
insideField,
|
|
15045
|
+
insideControl,
|
|
15046
|
+
label,
|
|
15047
|
+
message,
|
|
15048
|
+
horizontal,
|
|
15049
|
+
fieldClassName,
|
|
15050
|
+
});
|
|
14789
15051
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
14790
15052
|
label,
|
|
14791
15053
|
id: baseProps.id,
|
|
14792
15054
|
labelProps,
|
|
14793
15055
|
// Inline mode renders a bare picker with no input to label.
|
|
14794
|
-
rendersLabel:
|
|
15056
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
14795
15057
|
});
|
|
14796
15058
|
const helpClass = usePrefixedClassNames('help', {
|
|
14797
15059
|
[`is-${messageColor}`]: !!messageColor,
|
|
14798
15060
|
});
|
|
14799
|
-
// The
|
|
14800
|
-
//
|
|
14801
|
-
let content = (jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps
|
|
15061
|
+
// The base hides its launcher while the Control it sits in is loading,
|
|
15062
|
+
// whether that is the one rendered below or an enclosing one.
|
|
15063
|
+
let content = (jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps }));
|
|
14802
15064
|
// Inline mode renders a bare picker with no input, so the Control's
|
|
14803
15065
|
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
14804
15066
|
if (!insideControl && !baseProps.inline) {
|
|
14805
15067
|
content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
14806
15068
|
}
|
|
14807
15069
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
14808
|
-
if (
|
|
15070
|
+
if (ownField) {
|
|
14809
15071
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
14810
15072
|
}
|
|
14811
15073
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|