@allxsmith/bestax-bulma 5.26.2 → 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 +992 -580
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +992 -580
- package/dist/index.esm.js.map +1 -1
- package/dist/types/elements/Progress.d.ts +6 -1
- package/dist/types/form/Checkboxes.d.ts +7 -0
- package/dist/types/form/Control.d.ts +14 -2
- package/dist/types/form/DateInput.d.ts +28 -3
- package/dist/types/form/DateInputBase.d.ts +17 -3
- package/dist/types/form/DateTimeInput.d.ts +28 -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 +21 -4
- package/dist/types/form/InputBase.d.ts +5 -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 +13 -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 +9 -2
- package/dist/types/form/TextAreaBase.d.ts +12 -3
- package/dist/types/form/TimeInput.d.ts +28 -3
- package/dist/types/form/TimeInputBase.d.ts +21 -4
- package/dist/types/form/_pickerInternals/Calendar.d.ts +17 -2
- package/dist/types/form/_pickerInternals/PickerPopover.d.ts +6 -0
- package/dist/types/form/_pickerInternals/dateUtils.d.ts +17 -0
- package/dist/types/form/controlLoading.d.ts +4 -0
- package/dist/types/helpers/Theme.d.ts +13 -4
- package/dist/types/helpers/portal.d.ts +2 -1
- package/dist/types/helpers/useAnchoredPosition.d.ts +10 -1
- package/dist/types/helpers/useFocusTrap.d.ts +13 -5
- package/dist/types/helpers/useOtherClasses.d.ts +13 -1
- package/dist/types-cjs/elements/Progress.d.ts +6 -1
- package/dist/types-cjs/form/Checkboxes.d.ts +7 -0
- package/dist/types-cjs/form/Control.d.ts +14 -2
- package/dist/types-cjs/form/DateInput.d.ts +28 -3
- package/dist/types-cjs/form/DateInputBase.d.ts +17 -3
- package/dist/types-cjs/form/DateTimeInput.d.ts +28 -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 +21 -4
- package/dist/types-cjs/form/InputBase.d.ts +5 -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 +13 -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 +9 -2
- package/dist/types-cjs/form/TextAreaBase.d.ts +12 -3
- package/dist/types-cjs/form/TimeInput.d.ts +28 -3
- package/dist/types-cjs/form/TimeInputBase.d.ts +21 -4
- package/dist/types-cjs/form/_pickerInternals/Calendar.d.ts +17 -2
- package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +6 -0
- 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/Theme.d.ts +13 -4
- package/dist/types-cjs/helpers/portal.d.ts +2 -1
- package/dist/types-cjs/helpers/useAnchoredPosition.d.ts +10 -1
- package/dist/types-cjs/helpers/useFocusTrap.d.ts +13 -5
- package/dist/types-cjs/helpers/useOtherClasses.d.ts +13 -1
- 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/elements/_index.scss +2 -0
- package/src/scss/elements/_progress.scss +17 -0
- package/src/scss/elements/_skeleton.scss +21 -0
- package/src/scss/form/_datetimeinput.scss +9 -0
- package/src/scss/form/_picker-popover.scss +23 -4
- package/src/scss/form/_rate.scss +3 -1
- package/src/scss/form/_timeinput.scss +12 -2
package/dist/index.esm.js
CHANGED
|
@@ -2939,7 +2939,8 @@ function resolvePortalContainer(container) {
|
|
|
2939
2939
|
* Tab order and focus follow the DOM too. Content portaled from inside a
|
|
2940
2940
|
* focus trap is no longer inside the trapped element, so the trap neither
|
|
2941
2941
|
* reaches it nor holds focus there: render nested overlays inside the
|
|
2942
|
-
* trapped element, or give them a trap of their own.
|
|
2942
|
+
* trapped element, or give them a trap of their own. To trap portaled
|
|
2943
|
+
* content, put `useFocusTrap`'s ref on the element inside the `Portal`.
|
|
2943
2944
|
*
|
|
2944
2945
|
* @function
|
|
2945
2946
|
* @param {PortalProps} props - Props for the Portal component.
|
|
@@ -5195,12 +5196,20 @@ const focusedElement = (doc) => getDeepestActiveElement(doc) ?? doc.body;
|
|
|
5195
5196
|
* behaviour, and a modal also has to block the page behind it, which `Modal`
|
|
5196
5197
|
* does. Content the container renders through a portal lives elsewhere in
|
|
5197
5198
|
* the DOM, so it is outside the trap: render nested overlays inside the
|
|
5198
|
-
* container, or give them a trap of their own.
|
|
5199
|
-
*
|
|
5200
|
-
*
|
|
5201
|
-
*
|
|
5202
|
-
*
|
|
5203
|
-
*
|
|
5199
|
+
* container, or give them a trap of their own. To trap content a `Portal`
|
|
5200
|
+
* renders, pass a ref to the element inside the `Portal` as `containerRef`.
|
|
5201
|
+
*
|
|
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.
|
|
5204
5213
|
*
|
|
5205
5214
|
* @function useFocusTrap
|
|
5206
5215
|
* @param containerRef - Ref to the element focus stays inside.
|
|
@@ -5220,12 +5229,18 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5220
5229
|
// Portaled content has no server-rendered counterpart, so a portaled
|
|
5221
5230
|
// container only exists from the commit after hydration.
|
|
5222
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);
|
|
5223
5236
|
useEffect(() => {
|
|
5237
|
+
heldRef.current = null;
|
|
5224
5238
|
if (!active || !hydrated)
|
|
5225
5239
|
return undefined;
|
|
5226
5240
|
const container = containerRef.current;
|
|
5227
5241
|
if (!container)
|
|
5228
5242
|
return undefined;
|
|
5243
|
+
heldRef.current = container;
|
|
5229
5244
|
const doc = container.ownerDocument;
|
|
5230
5245
|
const opener = focusedElement(doc);
|
|
5231
5246
|
// `initialFocusRef` counts only when focusing it puts focus inside. Focus
|
|
@@ -5300,7 +5315,28 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5300
5315
|
target.focus();
|
|
5301
5316
|
}
|
|
5302
5317
|
};
|
|
5303
|
-
}, [
|
|
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
|
+
});
|
|
5304
5340
|
}
|
|
5305
5341
|
|
|
5306
5342
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
@@ -5312,6 +5348,15 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5312
5348
|
const useClientLayoutEffect = typeof document === 'undefined' ? useEffect : useLayoutEffect;
|
|
5313
5349
|
|
|
5314
5350
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
5351
|
+
/**
|
|
5352
|
+
* A `styledGap` for a panel whose stylesheet puts the gap in a margin on the
|
|
5353
|
+
* side facing the anchor, negative on a portaled panel above it, so the
|
|
5354
|
+
* larger magnitude is the gap. In pixels.
|
|
5355
|
+
*/
|
|
5356
|
+
function marginGap(panel) {
|
|
5357
|
+
const { marginTop, marginBottom } = getComputedStyle(panel);
|
|
5358
|
+
return Math.max(Math.abs(parseFloat(marginTop)) || 0, Math.abs(parseFloat(marginBottom)) || 0);
|
|
5359
|
+
}
|
|
5315
5360
|
/**
|
|
5316
5361
|
* The corner that keeps a panel of this size in the viewport, preferring
|
|
5317
5362
|
* below the anchor and lined up with its left edge.
|
|
@@ -5536,15 +5581,6 @@ function nearestDialog(event) {
|
|
|
5536
5581
|
return role === 'dialog' || role === 'alertdialog';
|
|
5537
5582
|
}) ?? null);
|
|
5538
5583
|
}
|
|
5539
|
-
/**
|
|
5540
|
-
* The gap the stylesheet puts between the panel and the trigger, in pixels.
|
|
5541
|
-
* It is a margin on whichever side faces the trigger, and a negative one on
|
|
5542
|
-
* a portaled panel above it, so the larger magnitude is the gap.
|
|
5543
|
-
*/
|
|
5544
|
-
function styledGap(panel) {
|
|
5545
|
-
const { marginTop, marginBottom } = getComputedStyle(panel);
|
|
5546
|
-
return Math.max(Math.abs(parseFloat(marginTop)) || 0, Math.abs(parseFloat(marginBottom)) || 0);
|
|
5547
|
-
}
|
|
5548
5584
|
/**
|
|
5549
5585
|
* The `Popover` component opens a panel of interactive content, such as a filter form or a set of share options, anchored to the element that toggles it.
|
|
5550
5586
|
*
|
|
@@ -5596,7 +5632,7 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
|
|
|
5596
5632
|
// The gap comes from `--bulma-popover-offset` in the stylesheet, so the
|
|
5597
5633
|
// coordinates leave it out and `auto` reads it off the panel.
|
|
5598
5634
|
offset: 0,
|
|
5599
|
-
styledGap,
|
|
5635
|
+
styledGap: marginGap,
|
|
5600
5636
|
});
|
|
5601
5637
|
// The trigger renders first inside the wrapper. Read when needed rather
|
|
5602
5638
|
// than held, so a trigger that remounted is still the one found.
|
|
@@ -8733,6 +8769,56 @@ const useInsideControl = () => useContext(ControlContext);
|
|
|
8733
8769
|
const FieldProvider = FieldContext.Provider;
|
|
8734
8770
|
/** Provider for Control context — used internally by Control component. */
|
|
8735
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
|
+
};
|
|
8736
8822
|
const FieldLabelIdContext = createContext(undefined);
|
|
8737
8823
|
/**
|
|
8738
8824
|
* The id a labeled Field wants its single composed control to adopt (#495).
|
|
@@ -8863,6 +8949,18 @@ const Checkbox = forwardRef(({ color, size, className, children, textColor, disa
|
|
|
8863
8949
|
});
|
|
8864
8950
|
Checkbox.displayName = 'Checkbox';
|
|
8865
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
|
+
|
|
8866
8964
|
const allowedColors = [...validColors, 'inherit', 'current'];
|
|
8867
8965
|
/**
|
|
8868
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.
|
|
@@ -8922,7 +9020,7 @@ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isL
|
|
|
8922
9020
|
});
|
|
8923
9021
|
const controlClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
8924
9022
|
// --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
|
|
8925
|
-
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 &&
|
|
8926
9024
|
(isIconProps(leftIconValue) ? (jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
|
|
8927
9025
|
(isIconProps(rightIconValue) ? (jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
|
|
8928
9026
|
});
|
|
@@ -9147,10 +9245,18 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, }) {
|
|
|
9147
9245
|
const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
|
|
9148
9246
|
const insideField = useInsideField();
|
|
9149
9247
|
const insideControl = useInsideControl();
|
|
9248
|
+
const ownField = rendersOwnField('Checkboxes', {
|
|
9249
|
+
insideField,
|
|
9250
|
+
insideControl,
|
|
9251
|
+
label,
|
|
9252
|
+
message,
|
|
9253
|
+
horizontal,
|
|
9254
|
+
fieldClassName,
|
|
9255
|
+
});
|
|
9150
9256
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
9151
9257
|
label,
|
|
9152
9258
|
labelProps,
|
|
9153
|
-
rendersLabel:
|
|
9259
|
+
rendersLabel: ownField,
|
|
9154
9260
|
});
|
|
9155
9261
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
9156
9262
|
...props,
|
|
@@ -9179,7 +9285,7 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
|
|
|
9179
9285
|
if (!insideControl) {
|
|
9180
9286
|
content = jsx(Control, { children: content });
|
|
9181
9287
|
}
|
|
9182
|
-
if (
|
|
9288
|
+
if (ownField) {
|
|
9183
9289
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9184
9290
|
}
|
|
9185
9291
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -9198,11 +9304,20 @@ const File = forwardRef(({
|
|
|
9198
9304
|
// Field props
|
|
9199
9305
|
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
|
|
9200
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
|
+
});
|
|
9201
9316
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
9202
9317
|
label,
|
|
9203
9318
|
id: props.id,
|
|
9204
9319
|
labelProps,
|
|
9205
|
-
rendersLabel:
|
|
9320
|
+
rendersLabel: ownField,
|
|
9206
9321
|
});
|
|
9207
9322
|
const { classPrefix } = useConfig();
|
|
9208
9323
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
@@ -9234,7 +9349,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
9234
9349
|
});
|
|
9235
9350
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
9236
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 }))] }) }));
|
|
9237
|
-
if (
|
|
9352
|
+
if (ownField) {
|
|
9238
9353
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
|
|
9239
9354
|
}
|
|
9240
9355
|
return (jsxs(Fragment, { children: [fileElement, messageEl] }));
|
|
@@ -9367,10 +9482,18 @@ Radio.displayName = 'Radio';
|
|
|
9367
9482
|
const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
|
|
9368
9483
|
const insideField = useInsideField();
|
|
9369
9484
|
const insideControl = useInsideControl();
|
|
9485
|
+
const ownField = rendersOwnField('Radios', {
|
|
9486
|
+
insideField,
|
|
9487
|
+
insideControl,
|
|
9488
|
+
label,
|
|
9489
|
+
message,
|
|
9490
|
+
horizontal,
|
|
9491
|
+
fieldClassName,
|
|
9492
|
+
});
|
|
9370
9493
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
9371
9494
|
label,
|
|
9372
9495
|
labelProps,
|
|
9373
|
-
rendersLabel:
|
|
9496
|
+
rendersLabel: ownField,
|
|
9374
9497
|
});
|
|
9375
9498
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
9376
9499
|
...props,
|
|
@@ -9402,7 +9525,7 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
|
|
|
9402
9525
|
if (!insideControl) {
|
|
9403
9526
|
content = jsx(Control, { children: content });
|
|
9404
9527
|
}
|
|
9405
|
-
if (
|
|
9528
|
+
if (ownField) {
|
|
9406
9529
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9407
9530
|
}
|
|
9408
9531
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -9582,6 +9705,14 @@ const Slider = forwardRef((props, ref) => {
|
|
|
9582
9705
|
range: _range, value: _value, defaultValue: _defaultValue, onChange: _onChange, ariaLabel: _ariaLabel, minDistance: _minDistance, nameLow: _nameLow, nameHigh: _nameHigh, ...restProps } = props;
|
|
9583
9706
|
const insideField = useInsideField();
|
|
9584
9707
|
const insideControl = useInsideControl();
|
|
9708
|
+
const ownField = rendersOwnField('Slider', {
|
|
9709
|
+
insideField,
|
|
9710
|
+
insideControl,
|
|
9711
|
+
label,
|
|
9712
|
+
message,
|
|
9713
|
+
horizontal,
|
|
9714
|
+
fieldClassName,
|
|
9715
|
+
});
|
|
9585
9716
|
const { bulmaHelperClasses, rest } = useBulmaClasses(restProps);
|
|
9586
9717
|
// In range mode the low thumb carries `rest` (and any user id), so the
|
|
9587
9718
|
// label targets it; each thumb keeps its own aria-label for AT.
|
|
@@ -9589,7 +9720,7 @@ const Slider = forwardRef((props, ref) => {
|
|
|
9589
9720
|
label,
|
|
9590
9721
|
id: restProps.id,
|
|
9591
9722
|
labelProps,
|
|
9592
|
-
rendersLabel:
|
|
9723
|
+
rendersLabel: ownField,
|
|
9593
9724
|
});
|
|
9594
9725
|
// Resolve tooltip mode: explicit tooltip prop takes precedence, else showOutput maps to 'auto'
|
|
9595
9726
|
const tooltipMode = tooltip ?? (showOutput ? 'auto' : 'hidden');
|
|
@@ -9928,7 +10059,7 @@ const Slider = forwardRef((props, ref) => {
|
|
|
9928
10059
|
if (!insideControl) {
|
|
9929
10060
|
content = jsx(Control, { children: content });
|
|
9930
10061
|
}
|
|
9931
|
-
if (
|
|
10062
|
+
if (ownField) {
|
|
9932
10063
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9933
10064
|
}
|
|
9934
10065
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -10158,10 +10289,18 @@ function getFillPercent(iconIndex, value) {
|
|
|
10158
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) => {
|
|
10159
10290
|
const insideField = useInsideField();
|
|
10160
10291
|
const insideControl = useInsideControl();
|
|
10292
|
+
const ownField = rendersOwnField('Rate', {
|
|
10293
|
+
insideField,
|
|
10294
|
+
insideControl,
|
|
10295
|
+
label,
|
|
10296
|
+
message,
|
|
10297
|
+
horizontal,
|
|
10298
|
+
fieldClassName,
|
|
10299
|
+
});
|
|
10161
10300
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
10162
10301
|
label,
|
|
10163
10302
|
labelProps,
|
|
10164
|
-
rendersLabel:
|
|
10303
|
+
rendersLabel: ownField,
|
|
10165
10304
|
});
|
|
10166
10305
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
10167
10306
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
@@ -10364,7 +10503,7 @@ const Rate = forwardRef(({ label, labelSize, labelProps, horizontal, message, me
|
|
|
10364
10503
|
if (!insideControl) {
|
|
10365
10504
|
content = jsx(Control, { children: content });
|
|
10366
10505
|
}
|
|
10367
|
-
if (
|
|
10506
|
+
if (ownField) {
|
|
10368
10507
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
10369
10508
|
}
|
|
10370
10509
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -10645,6 +10784,15 @@ const Taginput = forwardRef(({
|
|
|
10645
10784
|
// Field props
|
|
10646
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) => {
|
|
10647
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
|
+
});
|
|
10648
10796
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
10649
10797
|
const { classPrefix } = useConfig();
|
|
10650
10798
|
const defaultIconLibrary = useIconLibrary();
|
|
@@ -10666,7 +10814,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
10666
10814
|
label,
|
|
10667
10815
|
id,
|
|
10668
10816
|
labelProps,
|
|
10669
|
-
rendersLabel:
|
|
10817
|
+
rendersLabel: ownField && !isMaxReached,
|
|
10670
10818
|
});
|
|
10671
10819
|
// Get display value from tag
|
|
10672
10820
|
const getDisplayValue = (tag) => {
|
|
@@ -10938,7 +11086,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
10938
11086
|
'is-active': index === highlightedIndex,
|
|
10939
11087
|
}), onClick: () => addTag(item), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": index === highlightedIndex, children: item }, index))) }) })), showCounter && jsx("small", { className: counterClasses, children: counterText }), name &&
|
|
10940
11088
|
tags.map((tag, i) => (jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
|
|
10941
|
-
if (
|
|
11089
|
+
if (ownField) {
|
|
10942
11090
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
|
|
10943
11091
|
}
|
|
10944
11092
|
return (jsxs(Fragment, { children: [taginputElement, messageEl] }));
|
|
@@ -11060,7 +11208,7 @@ const TextAreaBase = forwardRef(({ color, size, isRounded, isStatic, isHovered,
|
|
|
11060
11208
|
TextAreaBase.displayName = 'TextAreaBase';
|
|
11061
11209
|
|
|
11062
11210
|
/**
|
|
11063
|
-
* The `Input` component provides a Bulma-styled text input, supporting colors, sizes, rounded corners, static
|
|
11211
|
+
* The `Input` component provides a Bulma-styled text input, supporting colors, sizes, rounded corners, static state, hover/focus/loading states, and all Bulma helper props.
|
|
11064
11212
|
*
|
|
11065
11213
|
* @function
|
|
11066
11214
|
* @param {InputProps} props - Props for Input.
|
|
@@ -11091,11 +11239,19 @@ fieldClassName, controlClassName,
|
|
|
11091
11239
|
...inputProps }, ref) => {
|
|
11092
11240
|
const insideField = useInsideField();
|
|
11093
11241
|
const insideControl = useInsideControl();
|
|
11242
|
+
const ownField = rendersOwnField('Input', {
|
|
11243
|
+
insideField,
|
|
11244
|
+
insideControl,
|
|
11245
|
+
label,
|
|
11246
|
+
message,
|
|
11247
|
+
horizontal,
|
|
11248
|
+
fieldClassName,
|
|
11249
|
+
});
|
|
11094
11250
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11095
11251
|
label,
|
|
11096
11252
|
id: inputProps.id,
|
|
11097
11253
|
labelProps,
|
|
11098
|
-
rendersLabel:
|
|
11254
|
+
rendersLabel: ownField,
|
|
11099
11255
|
});
|
|
11100
11256
|
const helpClass = usePrefixedClassNames('help', {
|
|
11101
11257
|
[`is-${messageColor}`]: !!messageColor,
|
|
@@ -11105,7 +11261,7 @@ fieldClassName, controlClassName,
|
|
|
11105
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 }));
|
|
11106
11262
|
}
|
|
11107
11263
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
11108
|
-
if (
|
|
11264
|
+
if (ownField) {
|
|
11109
11265
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11110
11266
|
}
|
|
11111
11267
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -11141,11 +11297,19 @@ fieldClassName, controlClassName,
|
|
|
11141
11297
|
...selectProps }, ref) => {
|
|
11142
11298
|
const insideField = useInsideField();
|
|
11143
11299
|
const insideControl = useInsideControl();
|
|
11300
|
+
const ownField = rendersOwnField('Select', {
|
|
11301
|
+
insideField,
|
|
11302
|
+
insideControl,
|
|
11303
|
+
label,
|
|
11304
|
+
message,
|
|
11305
|
+
horizontal,
|
|
11306
|
+
fieldClassName,
|
|
11307
|
+
});
|
|
11144
11308
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11145
11309
|
label,
|
|
11146
11310
|
id: selectProps.id,
|
|
11147
11311
|
labelProps,
|
|
11148
|
-
rendersLabel:
|
|
11312
|
+
rendersLabel: ownField,
|
|
11149
11313
|
});
|
|
11150
11314
|
const helpClass = usePrefixedClassNames('help', {
|
|
11151
11315
|
[`is-${messageColor}`]: !!messageColor,
|
|
@@ -11155,7 +11319,7 @@ fieldClassName, controlClassName,
|
|
|
11155
11319
|
content = (jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
11156
11320
|
}
|
|
11157
11321
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
11158
|
-
if (
|
|
11322
|
+
if (ownField) {
|
|
11159
11323
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11160
11324
|
}
|
|
11161
11325
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -11163,7 +11327,7 @@ fieldClassName, controlClassName,
|
|
|
11163
11327
|
Select.displayName = 'Select';
|
|
11164
11328
|
|
|
11165
11329
|
/**
|
|
11166
|
-
* The `TextArea` component provides a Bulma-styled multi-line text input, supporting color, size,
|
|
11330
|
+
* The `TextArea` component provides a Bulma-styled multi-line text input, supporting color, size, hover/focus/loading states, fixed size, and all Bulma helper props.
|
|
11167
11331
|
*
|
|
11168
11332
|
* @function
|
|
11169
11333
|
* @param {TextAreaProps} props - Props for TextArea.
|
|
@@ -11192,11 +11356,19 @@ fieldClassName, controlClassName,
|
|
|
11192
11356
|
...textAreaProps }, ref) => {
|
|
11193
11357
|
const insideField = useInsideField();
|
|
11194
11358
|
const insideControl = useInsideControl();
|
|
11359
|
+
const ownField = rendersOwnField('TextArea', {
|
|
11360
|
+
insideField,
|
|
11361
|
+
insideControl,
|
|
11362
|
+
label,
|
|
11363
|
+
message,
|
|
11364
|
+
horizontal,
|
|
11365
|
+
fieldClassName,
|
|
11366
|
+
});
|
|
11195
11367
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11196
11368
|
label,
|
|
11197
11369
|
id: textAreaProps.id,
|
|
11198
11370
|
labelProps,
|
|
11199
|
-
rendersLabel:
|
|
11371
|
+
rendersLabel: ownField,
|
|
11200
11372
|
});
|
|
11201
11373
|
const helpClass = usePrefixedClassNames('help', {
|
|
11202
11374
|
[`is-${messageColor}`]: !!messageColor,
|
|
@@ -11206,7 +11378,7 @@ fieldClassName, controlClassName,
|
|
|
11206
11378
|
content = (jsx(Control, { isLoading: controlIsLoading, size: controlSize, className: controlClassName, children: content }));
|
|
11207
11379
|
}
|
|
11208
11380
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
11209
|
-
if (
|
|
11381
|
+
if (ownField) {
|
|
11210
11382
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11211
11383
|
}
|
|
11212
11384
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -11249,355 +11421,90 @@ const mergeLabels = (overrides) => ({
|
|
|
11249
11421
|
...(overrides ?? {}),
|
|
11250
11422
|
});
|
|
11251
11423
|
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
|
|
11255
|
-
|
|
11256
|
-
const DEFAULT_TIME_FORMAT_12 = 'hh:mm A';
|
|
11257
|
-
const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm';
|
|
11258
|
-
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
11259
|
-
const TOKEN_RE = /YYYY|YY|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|A|a/g;
|
|
11260
|
-
function formatToken(d, token) {
|
|
11261
|
-
switch (token) {
|
|
11262
|
-
case 'YYYY':
|
|
11263
|
-
// Zero-pad to a fixed 4 chars so the rendered width matches the
|
|
11264
|
-
// segment-map width during digit-by-digit keyboard entry (and to match
|
|
11265
|
-
// ISO 8601). Years >9999 keep their natural width.
|
|
11266
|
-
return pad(d.getFullYear(), 4);
|
|
11267
|
-
case 'YY':
|
|
11268
|
-
return pad(d.getFullYear() % 100);
|
|
11269
|
-
case 'MM':
|
|
11270
|
-
return pad(d.getMonth() + 1);
|
|
11271
|
-
case 'M':
|
|
11272
|
-
return String(d.getMonth() + 1);
|
|
11273
|
-
case 'DD':
|
|
11274
|
-
return pad(d.getDate());
|
|
11275
|
-
case 'D':
|
|
11276
|
-
return String(d.getDate());
|
|
11277
|
-
case 'HH':
|
|
11278
|
-
return pad(d.getHours());
|
|
11279
|
-
case 'H':
|
|
11280
|
-
return String(d.getHours());
|
|
11281
|
-
case 'hh': {
|
|
11282
|
-
const h12 = d.getHours() % 12 || 12;
|
|
11283
|
-
return pad(h12);
|
|
11284
|
-
}
|
|
11285
|
-
case 'h': {
|
|
11286
|
-
const h12 = d.getHours() % 12 || 12;
|
|
11287
|
-
return String(h12);
|
|
11288
|
-
}
|
|
11289
|
-
case 'mm':
|
|
11290
|
-
return pad(d.getMinutes());
|
|
11291
|
-
case 'm':
|
|
11292
|
-
return String(d.getMinutes());
|
|
11293
|
-
case 'ss':
|
|
11294
|
-
return pad(d.getSeconds());
|
|
11295
|
-
case 's':
|
|
11296
|
-
return String(d.getSeconds());
|
|
11297
|
-
case 'A':
|
|
11298
|
-
return d.getHours() < 12 ? 'AM' : 'PM';
|
|
11299
|
-
case 'a':
|
|
11300
|
-
return d.getHours() < 12 ? 'am' : 'pm';
|
|
11301
|
-
default:
|
|
11302
|
-
return token;
|
|
11303
|
-
}
|
|
11424
|
+
function isSameDay(a, b) {
|
|
11425
|
+
return (a.getFullYear() === b.getFullYear() &&
|
|
11426
|
+
a.getMonth() === b.getMonth() &&
|
|
11427
|
+
a.getDate() === b.getDate());
|
|
11304
11428
|
}
|
|
11305
|
-
function
|
|
11306
|
-
return
|
|
11429
|
+
function isSameMonth(a, b) {
|
|
11430
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
|
|
11307
11431
|
}
|
|
11308
|
-
function
|
|
11309
|
-
return
|
|
11432
|
+
function isBefore(a, b) {
|
|
11433
|
+
return a.getTime() < b.getTime();
|
|
11310
11434
|
}
|
|
11311
|
-
function
|
|
11312
|
-
|
|
11313
|
-
if (typeof f === 'string')
|
|
11314
|
-
return formatTokenString(d, f);
|
|
11315
|
-
return formatWithIntl(d, f, locale);
|
|
11435
|
+
function isAfter(a, b) {
|
|
11436
|
+
return a.getTime() > b.getTime();
|
|
11316
11437
|
}
|
|
11317
|
-
function
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11438
|
+
function isWithin(d, min, max) {
|
|
11439
|
+
if (min && isBefore(d, min))
|
|
11440
|
+
return false;
|
|
11441
|
+
if (max && isAfter(d, max))
|
|
11442
|
+
return false;
|
|
11443
|
+
return true;
|
|
11322
11444
|
}
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11445
|
+
// The boundaries build on `makeDate` (below), so a date in years 0–99 keeps
|
|
11446
|
+
// its year.
|
|
11447
|
+
function startOfDay(d) {
|
|
11448
|
+
return makeDate(d.getFullYear(), d.getMonth(), d.getDate());
|
|
11449
|
+
}
|
|
11450
|
+
function endOfDay(d) {
|
|
11451
|
+
const r = makeDate(d.getFullYear(), d.getMonth(), d.getDate());
|
|
11452
|
+
r.setHours(23, 59, 59, 999);
|
|
11453
|
+
return r;
|
|
11454
|
+
}
|
|
11455
|
+
function startOfMonth(d) {
|
|
11456
|
+
return makeDate(d.getFullYear(), d.getMonth());
|
|
11328
11457
|
}
|
|
11329
11458
|
/**
|
|
11330
|
-
*
|
|
11331
|
-
*
|
|
11332
|
-
*
|
|
11333
|
-
* Returns `null` when the cycle can't be read from the format — it's an
|
|
11334
|
-
* `Intl.DateTimeFormat` options object (no token string), `undefined`, or has
|
|
11335
|
-
* no hour token — so callers fall back to the raw `hourFormat` prop. Scans with
|
|
11336
|
-
* the same {@link TOKEN_RE} grammar the input renders with, so the derived cycle
|
|
11337
|
-
* always agrees with what the field actually displays.
|
|
11459
|
+
* Local midnight on `year`-`month`-`day` (`month` from 0), like the
|
|
11460
|
+
* `Date(y, m, d)` constructor except that years 0–99 stay as given rather than
|
|
11461
|
+
* turning into 1900–1999.
|
|
11338
11462
|
*/
|
|
11339
|
-
function
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
if (token === 'H' || token === 'HH')
|
|
11344
|
-
return '24';
|
|
11345
|
-
if (token === 'h' || token === 'hh')
|
|
11346
|
-
return '12';
|
|
11347
|
-
}
|
|
11348
|
-
return null;
|
|
11463
|
+
function makeDate(year, month = 0, day = 1) {
|
|
11464
|
+
const d = new Date(2000, 0, 1);
|
|
11465
|
+
d.setFullYear(year, month, day);
|
|
11466
|
+
return d;
|
|
11349
11467
|
}
|
|
11350
11468
|
/**
|
|
11351
|
-
*
|
|
11352
|
-
*
|
|
11353
|
-
*
|
|
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.
|
|
11354
11472
|
*/
|
|
11355
|
-
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
const year = parts.year ?? new Date().getFullYear();
|
|
11365
|
-
const month = (parts.month ?? 1) - 1;
|
|
11366
|
-
const day = parts.day ?? 1;
|
|
11367
|
-
const hours = parts.hours ?? 0;
|
|
11368
|
-
const minutes = parts.minutes ?? 0;
|
|
11369
|
-
const seconds = parts.seconds ?? 0;
|
|
11370
|
-
const d = new Date(year, month, day, hours, minutes, seconds, 0);
|
|
11371
|
-
if (d.getFullYear() !== year ||
|
|
11372
|
-
d.getMonth() !== month ||
|
|
11373
|
-
d.getDate() !== day) {
|
|
11374
|
-
return null;
|
|
11375
|
-
}
|
|
11376
|
-
return d;
|
|
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;
|
|
11377
11482
|
}
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
const
|
|
11386
|
-
|
|
11483
|
+
/**
|
|
11484
|
+
* First instant of the day, month or year containing `d`. Works by setters on
|
|
11485
|
+
* a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
|
|
11486
|
+
* as 1900–1999: segmented typing passes through such years on the way to a
|
|
11487
|
+
* four-digit one.
|
|
11488
|
+
*/
|
|
11489
|
+
function startOfPeriod(d, granularity) {
|
|
11490
|
+
const r = new Date(d);
|
|
11491
|
+
if (granularity === 'year')
|
|
11492
|
+
r.setMonth(0, 1);
|
|
11493
|
+
else if (granularity === 'month')
|
|
11494
|
+
r.setDate(1);
|
|
11495
|
+
r.setHours(0, 0, 0, 0);
|
|
11496
|
+
return r;
|
|
11387
11497
|
}
|
|
11388
|
-
|
|
11389
|
-
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
|
|
11398
|
-
const one = fmt[i];
|
|
11399
|
-
let token = null;
|
|
11400
|
-
if (four === 'YYYY')
|
|
11401
|
-
token = 'YYYY';
|
|
11402
|
-
else if (two === 'YY' ||
|
|
11403
|
-
two === 'MM' ||
|
|
11404
|
-
two === 'DD' ||
|
|
11405
|
-
two === 'HH' ||
|
|
11406
|
-
two === 'hh' ||
|
|
11407
|
-
two === 'mm' ||
|
|
11408
|
-
two === 'ss')
|
|
11409
|
-
token = two;
|
|
11410
|
-
else if (one === 'M' ||
|
|
11411
|
-
one === 'D' ||
|
|
11412
|
-
one === 'H' ||
|
|
11413
|
-
one === 'h' ||
|
|
11414
|
-
one === 'm' ||
|
|
11415
|
-
one === 's' ||
|
|
11416
|
-
one === 'A' ||
|
|
11417
|
-
one === 'a')
|
|
11418
|
-
token = one;
|
|
11419
|
-
if (token) {
|
|
11420
|
-
tokenOrder.push(token);
|
|
11421
|
-
switch (token) {
|
|
11422
|
-
case 'YYYY':
|
|
11423
|
-
regex += '(\\d{4})';
|
|
11424
|
-
break;
|
|
11425
|
-
case 'YY':
|
|
11426
|
-
regex += '(\\d{2})';
|
|
11427
|
-
break;
|
|
11428
|
-
case 'MM':
|
|
11429
|
-
case 'DD':
|
|
11430
|
-
case 'HH':
|
|
11431
|
-
case 'hh':
|
|
11432
|
-
case 'mm':
|
|
11433
|
-
case 'ss':
|
|
11434
|
-
regex += '(\\d{1,2})';
|
|
11435
|
-
break;
|
|
11436
|
-
case 'M':
|
|
11437
|
-
case 'D':
|
|
11438
|
-
case 'H':
|
|
11439
|
-
case 'h':
|
|
11440
|
-
case 'm':
|
|
11441
|
-
case 's':
|
|
11442
|
-
regex += '(\\d{1,2})';
|
|
11443
|
-
break;
|
|
11444
|
-
case 'A':
|
|
11445
|
-
regex += '(AM|PM)';
|
|
11446
|
-
break;
|
|
11447
|
-
case 'a':
|
|
11448
|
-
regex += '(am|pm)';
|
|
11449
|
-
break;
|
|
11450
|
-
}
|
|
11451
|
-
i += token.length;
|
|
11452
|
-
}
|
|
11453
|
-
else {
|
|
11454
|
-
regex += escapeRegExp(one);
|
|
11455
|
-
i += 1;
|
|
11456
|
-
}
|
|
11457
|
-
}
|
|
11458
|
-
const m = new RegExp('^' + regex + '$').exec(input);
|
|
11459
|
-
if (!m)
|
|
11460
|
-
return null;
|
|
11461
|
-
const out = {};
|
|
11462
|
-
tokenOrder.forEach((token, idx) => {
|
|
11463
|
-
const v = m[idx + 1];
|
|
11464
|
-
switch (token) {
|
|
11465
|
-
case 'YYYY':
|
|
11466
|
-
out.year = Number(v);
|
|
11467
|
-
break;
|
|
11468
|
-
case 'YY':
|
|
11469
|
-
out.year = 2000 + Number(v);
|
|
11470
|
-
break;
|
|
11471
|
-
case 'MM':
|
|
11472
|
-
case 'M':
|
|
11473
|
-
out.month = Number(v);
|
|
11474
|
-
break;
|
|
11475
|
-
case 'DD':
|
|
11476
|
-
case 'D':
|
|
11477
|
-
out.day = Number(v);
|
|
11478
|
-
break;
|
|
11479
|
-
case 'HH':
|
|
11480
|
-
case 'H':
|
|
11481
|
-
out.hours = Number(v);
|
|
11482
|
-
break;
|
|
11483
|
-
case 'hh':
|
|
11484
|
-
case 'h':
|
|
11485
|
-
out.hours = Number(v);
|
|
11486
|
-
break;
|
|
11487
|
-
case 'mm':
|
|
11488
|
-
case 'm':
|
|
11489
|
-
out.minutes = Number(v);
|
|
11490
|
-
break;
|
|
11491
|
-
case 'ss':
|
|
11492
|
-
case 's':
|
|
11493
|
-
out.seconds = Number(v);
|
|
11494
|
-
break;
|
|
11495
|
-
case 'A':
|
|
11496
|
-
out.meridiem = v.toLowerCase() === 'am' ? 'am' : 'pm';
|
|
11497
|
-
break;
|
|
11498
|
-
case 'a':
|
|
11499
|
-
out.meridiem = v === 'am' ? 'am' : 'pm';
|
|
11500
|
-
break;
|
|
11501
|
-
}
|
|
11502
|
-
});
|
|
11503
|
-
if (out.meridiem !== undefined && out.hours !== undefined) {
|
|
11504
|
-
if (out.meridiem === 'pm' && out.hours < 12)
|
|
11505
|
-
out.hours += 12;
|
|
11506
|
-
if (out.meridiem === 'am' && out.hours === 12)
|
|
11507
|
-
out.hours = 0;
|
|
11508
|
-
}
|
|
11509
|
-
return out;
|
|
11510
|
-
}
|
|
11511
|
-
/**
|
|
11512
|
-
* Locale-aware day name labels in calendar order (Sunday → Saturday).
|
|
11513
|
-
* Caller rotates by `firstDayOfWeek`.
|
|
11514
|
-
*/
|
|
11515
|
-
function getDayNames(locale, length = 'short') {
|
|
11516
|
-
const fmt = new Intl.DateTimeFormat(locale, { weekday: length });
|
|
11517
|
-
// 2021-01-03 was a Sunday; use it as the anchor.
|
|
11518
|
-
const sunday = new Date(2021, 0, 3);
|
|
11519
|
-
const out = [];
|
|
11520
|
-
for (let i = 0; i < 7; i++) {
|
|
11521
|
-
const d = new Date(sunday);
|
|
11522
|
-
d.setDate(sunday.getDate() + i);
|
|
11523
|
-
out.push(fmt.format(d));
|
|
11524
|
-
}
|
|
11525
|
-
return out;
|
|
11526
|
-
}
|
|
11527
|
-
function getMonthNames(locale, length = 'long') {
|
|
11528
|
-
const fmt = new Intl.DateTimeFormat(locale, { month: length });
|
|
11529
|
-
const out = [];
|
|
11530
|
-
for (let i = 0; i < 12; i++) {
|
|
11531
|
-
out.push(fmt.format(new Date(2021, i, 1)));
|
|
11532
|
-
}
|
|
11533
|
-
return out;
|
|
11534
|
-
}
|
|
11535
|
-
|
|
11536
|
-
function isSameDay(a, b) {
|
|
11537
|
-
return (a.getFullYear() === b.getFullYear() &&
|
|
11538
|
-
a.getMonth() === b.getMonth() &&
|
|
11539
|
-
a.getDate() === b.getDate());
|
|
11540
|
-
}
|
|
11541
|
-
function isSameMonth(a, b) {
|
|
11542
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
|
|
11543
|
-
}
|
|
11544
|
-
function isBefore(a, b) {
|
|
11545
|
-
return a.getTime() < b.getTime();
|
|
11546
|
-
}
|
|
11547
|
-
function isAfter(a, b) {
|
|
11548
|
-
return a.getTime() > b.getTime();
|
|
11549
|
-
}
|
|
11550
|
-
function isWithin(d, min, max) {
|
|
11551
|
-
if (min && isBefore(d, min))
|
|
11552
|
-
return false;
|
|
11553
|
-
if (max && isAfter(d, max))
|
|
11554
|
-
return false;
|
|
11555
|
-
return true;
|
|
11556
|
-
}
|
|
11557
|
-
function startOfDay(d) {
|
|
11558
|
-
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
|
11559
|
-
}
|
|
11560
|
-
function endOfDay(d) {
|
|
11561
|
-
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
|
|
11562
|
-
}
|
|
11563
|
-
function startOfMonth(d) {
|
|
11564
|
-
return new Date(d.getFullYear(), d.getMonth(), 1, 0, 0, 0, 0);
|
|
11565
|
-
}
|
|
11566
|
-
/**
|
|
11567
|
-
* Local midnight on `year`-`month`-`day` (`month` from 0), like the
|
|
11568
|
-
* `Date(y, m, d)` constructor except that years 0–99 stay as given rather than
|
|
11569
|
-
* turning into 1900–1999.
|
|
11570
|
-
*/
|
|
11571
|
-
function makeDate(year, month = 0, day = 1) {
|
|
11572
|
-
const d = new Date(2000, 0, 1);
|
|
11573
|
-
d.setFullYear(year, month, day);
|
|
11574
|
-
return d;
|
|
11575
|
-
}
|
|
11576
|
-
/**
|
|
11577
|
-
* First instant of the day, month or year containing `d`. Works by setters on
|
|
11578
|
-
* a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
|
|
11579
|
-
* as 1900–1999: segmented typing passes through such years on the way to a
|
|
11580
|
-
* four-digit one.
|
|
11581
|
-
*/
|
|
11582
|
-
function startOfPeriod(d, granularity) {
|
|
11583
|
-
const r = new Date(d);
|
|
11584
|
-
if (granularity === 'year')
|
|
11585
|
-
r.setMonth(0, 1);
|
|
11586
|
-
else if (granularity === 'month')
|
|
11587
|
-
r.setDate(1);
|
|
11588
|
-
r.setHours(0, 0, 0, 0);
|
|
11589
|
-
return r;
|
|
11590
|
-
}
|
|
11591
|
-
/** Last instant of the day, month or year containing `d`. */
|
|
11592
|
-
function endOfPeriod(d, granularity) {
|
|
11593
|
-
const r = new Date(d);
|
|
11594
|
-
if (granularity === 'year')
|
|
11595
|
-
r.setMonth(11, 31);
|
|
11596
|
-
// Day 0 of the next month is this month's last day.
|
|
11597
|
-
else if (granularity === 'month')
|
|
11598
|
-
r.setMonth(r.getMonth() + 1, 0);
|
|
11599
|
-
r.setHours(23, 59, 59, 999);
|
|
11600
|
-
return r;
|
|
11498
|
+
/** Last instant of the day, month or year containing `d`. */
|
|
11499
|
+
function endOfPeriod(d, granularity) {
|
|
11500
|
+
const r = new Date(d);
|
|
11501
|
+
if (granularity === 'year')
|
|
11502
|
+
r.setMonth(11, 31);
|
|
11503
|
+
// Day 0 of the next month is this month's last day.
|
|
11504
|
+
else if (granularity === 'month')
|
|
11505
|
+
r.setMonth(r.getMonth() + 1, 0);
|
|
11506
|
+
r.setHours(23, 59, 59, 999);
|
|
11507
|
+
return r;
|
|
11601
11508
|
}
|
|
11602
11509
|
/**
|
|
11603
11510
|
* Whether the day `d` falls outside `[min, max]` or is blocked by
|
|
@@ -11649,7 +11556,7 @@ function addMonths(d, n) {
|
|
|
11649
11556
|
r.setDate(1);
|
|
11650
11557
|
r.setMonth(r.getMonth() + n);
|
|
11651
11558
|
// Clamp day to last day of new month if original day is out of range.
|
|
11652
|
-
const lastDay =
|
|
11559
|
+
const lastDay = makeDate(r.getFullYear(), r.getMonth() + 1, 0).getDate();
|
|
11653
11560
|
r.setDate(Math.min(day, lastDay));
|
|
11654
11561
|
return r;
|
|
11655
11562
|
}
|
|
@@ -11666,12 +11573,14 @@ function setTimeOfDay(d, parts) {
|
|
|
11666
11573
|
r.setSeconds(parts.seconds);
|
|
11667
11574
|
return r;
|
|
11668
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
|
+
*/
|
|
11669
11581
|
function clampDate(d, min, max) {
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
if (max && isAfter(d, max))
|
|
11673
|
-
return new Date(max);
|
|
11674
|
-
return new Date(d);
|
|
11582
|
+
const capped = max && isAfter(d, max) ? max : d;
|
|
11583
|
+
return new Date(min && isBefore(capped, min) ? min : capped);
|
|
11675
11584
|
}
|
|
11676
11585
|
/**
|
|
11677
11586
|
* Snap a Date's time-of-day to the nearest grid defined by the given
|
|
@@ -11693,44 +11602,337 @@ function snapTimeToIncrement(d, opts = {}) {
|
|
|
11693
11602
|
s -= 60;
|
|
11694
11603
|
m += 1;
|
|
11695
11604
|
}
|
|
11696
|
-
}
|
|
11697
|
-
if (incrementMinutes > 1) {
|
|
11698
|
-
m = Math.round(m / incrementMinutes) * incrementMinutes;
|
|
11699
|
-
if (m >= 60) {
|
|
11700
|
-
m -= 60;
|
|
11701
|
-
h += 1;
|
|
11605
|
+
}
|
|
11606
|
+
if (incrementMinutes > 1) {
|
|
11607
|
+
m = Math.round(m / incrementMinutes) * incrementMinutes;
|
|
11608
|
+
if (m >= 60) {
|
|
11609
|
+
m -= 60;
|
|
11610
|
+
h += 1;
|
|
11611
|
+
}
|
|
11612
|
+
}
|
|
11613
|
+
if (incrementHours > 1) {
|
|
11614
|
+
h = Math.round(h / incrementHours) * incrementHours;
|
|
11615
|
+
// setHours handles 24+ overflow naturally (rolls forward into next day).
|
|
11616
|
+
}
|
|
11617
|
+
r.setHours(h, m, s, 0);
|
|
11618
|
+
return r;
|
|
11619
|
+
}
|
|
11620
|
+
/**
|
|
11621
|
+
* Build a 6-week × 7-day grid (42 cells) anchored on the month containing
|
|
11622
|
+
* `monthAnchor`. The first cell is the start of the week containing the first
|
|
11623
|
+
* of the month, where the week starts on `firstDayOfWeek`.
|
|
11624
|
+
*/
|
|
11625
|
+
function buildMonthGrid(monthAnchor, firstDayOfWeek = 0) {
|
|
11626
|
+
const monthStart = startOfMonth(monthAnchor);
|
|
11627
|
+
const firstWeekday = monthStart.getDay();
|
|
11628
|
+
const offset = (firstWeekday - firstDayOfWeek + 7) % 7;
|
|
11629
|
+
const gridStart = addDays(monthStart, -offset);
|
|
11630
|
+
const today = startOfDay(new Date());
|
|
11631
|
+
const cells = [];
|
|
11632
|
+
for (let i = 0; i < 42; i++) {
|
|
11633
|
+
const date = addDays(gridStart, i);
|
|
11634
|
+
cells.push({
|
|
11635
|
+
date,
|
|
11636
|
+
inCurrentMonth: date.getMonth() === monthAnchor.getMonth(),
|
|
11637
|
+
isToday: isSameDay(date, today),
|
|
11638
|
+
});
|
|
11639
|
+
}
|
|
11640
|
+
return cells;
|
|
11641
|
+
}
|
|
11642
|
+
|
|
11643
|
+
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
|
|
11644
|
+
const DEFAULT_MONTH_FORMAT = 'YYYY-MM';
|
|
11645
|
+
const DEFAULT_YEAR_FORMAT = 'YYYY';
|
|
11646
|
+
const DEFAULT_TIME_FORMAT_24 = 'HH:mm';
|
|
11647
|
+
const DEFAULT_TIME_FORMAT_12 = 'hh:mm A';
|
|
11648
|
+
const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm';
|
|
11649
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
11650
|
+
const TOKEN_RE = /YYYY|YY|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|A|a/g;
|
|
11651
|
+
function formatToken(d, token) {
|
|
11652
|
+
switch (token) {
|
|
11653
|
+
case 'YYYY':
|
|
11654
|
+
// Zero-pad to a fixed 4 chars so the rendered width matches the
|
|
11655
|
+
// segment-map width during digit-by-digit keyboard entry (and to match
|
|
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.
|
|
11659
|
+
return pad(d.getFullYear(), 4);
|
|
11660
|
+
case 'YY':
|
|
11661
|
+
return pad(d.getFullYear() % 100);
|
|
11662
|
+
case 'MM':
|
|
11663
|
+
return pad(d.getMonth() + 1);
|
|
11664
|
+
case 'M':
|
|
11665
|
+
return String(d.getMonth() + 1);
|
|
11666
|
+
case 'DD':
|
|
11667
|
+
return pad(d.getDate());
|
|
11668
|
+
case 'D':
|
|
11669
|
+
return String(d.getDate());
|
|
11670
|
+
case 'HH':
|
|
11671
|
+
return pad(d.getHours());
|
|
11672
|
+
case 'H':
|
|
11673
|
+
return String(d.getHours());
|
|
11674
|
+
case 'hh': {
|
|
11675
|
+
const h12 = d.getHours() % 12 || 12;
|
|
11676
|
+
return pad(h12);
|
|
11677
|
+
}
|
|
11678
|
+
case 'h': {
|
|
11679
|
+
const h12 = d.getHours() % 12 || 12;
|
|
11680
|
+
return String(h12);
|
|
11681
|
+
}
|
|
11682
|
+
case 'mm':
|
|
11683
|
+
return pad(d.getMinutes());
|
|
11684
|
+
case 'm':
|
|
11685
|
+
return String(d.getMinutes());
|
|
11686
|
+
case 'ss':
|
|
11687
|
+
return pad(d.getSeconds());
|
|
11688
|
+
case 's':
|
|
11689
|
+
return String(d.getSeconds());
|
|
11690
|
+
case 'A':
|
|
11691
|
+
return d.getHours() < 12 ? 'AM' : 'PM';
|
|
11692
|
+
case 'a':
|
|
11693
|
+
return d.getHours() < 12 ? 'am' : 'pm';
|
|
11694
|
+
default:
|
|
11695
|
+
return token;
|
|
11696
|
+
}
|
|
11697
|
+
}
|
|
11698
|
+
function formatTokenString(d, fmt) {
|
|
11699
|
+
return fmt.replace(TOKEN_RE, t => formatToken(d, t));
|
|
11700
|
+
}
|
|
11701
|
+
function formatWithIntl(d, options, locale) {
|
|
11702
|
+
return new Intl.DateTimeFormat(locale, options).format(d);
|
|
11703
|
+
}
|
|
11704
|
+
function formatDate(d, fmt, locale) {
|
|
11705
|
+
const f = fmt ?? DEFAULT_DATE_FORMAT;
|
|
11706
|
+
if (typeof f === 'string')
|
|
11707
|
+
return formatTokenString(d, f);
|
|
11708
|
+
return formatWithIntl(d, f, locale);
|
|
11709
|
+
}
|
|
11710
|
+
function formatTime(d, fmt, locale) {
|
|
11711
|
+
const f = fmt ?? DEFAULT_TIME_FORMAT_24;
|
|
11712
|
+
if (typeof f === 'string')
|
|
11713
|
+
return formatTokenString(d, f);
|
|
11714
|
+
return formatWithIntl(d, f, locale);
|
|
11715
|
+
}
|
|
11716
|
+
function formatDateTime(d, fmt, locale) {
|
|
11717
|
+
const f = fmt ?? DEFAULT_DATETIME_FORMAT;
|
|
11718
|
+
if (typeof f === 'string')
|
|
11719
|
+
return formatTokenString(d, f);
|
|
11720
|
+
return formatWithIntl(d, f, locale);
|
|
11721
|
+
}
|
|
11722
|
+
/**
|
|
11723
|
+
* Derive the hour cycle (`'12'` or `'24'`) a display format will render, from
|
|
11724
|
+
* its first hour token: `h`/`hh` → 12-hour with meridiem, `H`/`HH` → 24-hour.
|
|
11725
|
+
*
|
|
11726
|
+
* Returns `null` when the cycle can't be read from the format — it's an
|
|
11727
|
+
* `Intl.DateTimeFormat` options object (no token string), `undefined`, or has
|
|
11728
|
+
* no hour token — so callers fall back to the raw `hourFormat` prop. Scans with
|
|
11729
|
+
* the same {@link TOKEN_RE} grammar the input renders with, so the derived cycle
|
|
11730
|
+
* always agrees with what the field actually displays.
|
|
11731
|
+
*/
|
|
11732
|
+
function hourCycleFromFormat(fmt) {
|
|
11733
|
+
if (typeof fmt !== 'string')
|
|
11734
|
+
return null;
|
|
11735
|
+
for (const [token] of fmt.matchAll(TOKEN_RE)) {
|
|
11736
|
+
if (token === 'H' || token === 'HH')
|
|
11737
|
+
return '24';
|
|
11738
|
+
if (token === 'h' || token === 'hh')
|
|
11739
|
+
return '12';
|
|
11740
|
+
}
|
|
11741
|
+
return null;
|
|
11742
|
+
}
|
|
11743
|
+
/**
|
|
11744
|
+
* Parse a date string against a token format. Returns null on mismatch.
|
|
11745
|
+
* For Intl-options formats parsing is the consumer's responsibility — pass
|
|
11746
|
+
* a custom `parse` prop on the picker.
|
|
11747
|
+
*/
|
|
11748
|
+
function parseDate(s, fmt,
|
|
11749
|
+
// locale arg accepted for symmetry; locale-aware parsing is not supported.
|
|
11750
|
+
_locale) {
|
|
11751
|
+
if (!s)
|
|
11752
|
+
return null;
|
|
11753
|
+
const format = fmt ?? DEFAULT_DATE_FORMAT;
|
|
11754
|
+
const parts = parseTokens(s, format);
|
|
11755
|
+
if (!parts)
|
|
11756
|
+
return null;
|
|
11757
|
+
const year = parts.year ?? new Date().getFullYear();
|
|
11758
|
+
const month = (parts.month ?? 1) - 1;
|
|
11759
|
+
const day = parts.day ?? 1;
|
|
11760
|
+
const hours = parts.hours ?? 0;
|
|
11761
|
+
const minutes = parts.minutes ?? 0;
|
|
11762
|
+
const seconds = parts.seconds ?? 0;
|
|
11763
|
+
const d = makeDate(year, month, day);
|
|
11764
|
+
d.setHours(hours, minutes, seconds, 0);
|
|
11765
|
+
if (d.getFullYear() !== year ||
|
|
11766
|
+
d.getMonth() !== month ||
|
|
11767
|
+
d.getDate() !== day) {
|
|
11768
|
+
return null;
|
|
11769
|
+
}
|
|
11770
|
+
return d;
|
|
11771
|
+
}
|
|
11772
|
+
function parseTime(s, fmt) {
|
|
11773
|
+
if (!s)
|
|
11774
|
+
return null;
|
|
11775
|
+
const format = fmt ?? DEFAULT_TIME_FORMAT_24;
|
|
11776
|
+
const parts = parseTokens(s, format);
|
|
11777
|
+
if (!parts)
|
|
11778
|
+
return null;
|
|
11779
|
+
const today = new Date();
|
|
11780
|
+
return new Date(today.getFullYear(), today.getMonth(), today.getDate(), parts.hours ?? 0, parts.minutes ?? 0, parts.seconds ?? 0, 0);
|
|
11781
|
+
}
|
|
11782
|
+
function escapeRegExp(s) {
|
|
11783
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11784
|
+
}
|
|
11785
|
+
function parseTokens(input, fmt) {
|
|
11786
|
+
const tokenOrder = [];
|
|
11787
|
+
let regex = '';
|
|
11788
|
+
let i = 0;
|
|
11789
|
+
while (i < fmt.length) {
|
|
11790
|
+
const four = fmt.slice(i, i + 4);
|
|
11791
|
+
const two = fmt.slice(i, i + 2);
|
|
11792
|
+
const one = fmt[i];
|
|
11793
|
+
let token = null;
|
|
11794
|
+
if (four === 'YYYY')
|
|
11795
|
+
token = 'YYYY';
|
|
11796
|
+
else if (two === 'YY' ||
|
|
11797
|
+
two === 'MM' ||
|
|
11798
|
+
two === 'DD' ||
|
|
11799
|
+
two === 'HH' ||
|
|
11800
|
+
two === 'hh' ||
|
|
11801
|
+
two === 'mm' ||
|
|
11802
|
+
two === 'ss')
|
|
11803
|
+
token = two;
|
|
11804
|
+
else if (one === 'M' ||
|
|
11805
|
+
one === 'D' ||
|
|
11806
|
+
one === 'H' ||
|
|
11807
|
+
one === 'h' ||
|
|
11808
|
+
one === 'm' ||
|
|
11809
|
+
one === 's' ||
|
|
11810
|
+
one === 'A' ||
|
|
11811
|
+
one === 'a')
|
|
11812
|
+
token = one;
|
|
11813
|
+
if (token) {
|
|
11814
|
+
tokenOrder.push(token);
|
|
11815
|
+
switch (token) {
|
|
11816
|
+
case 'YYYY':
|
|
11817
|
+
regex += '(\\d{4})';
|
|
11818
|
+
break;
|
|
11819
|
+
case 'YY':
|
|
11820
|
+
regex += '(\\d{2})';
|
|
11821
|
+
break;
|
|
11822
|
+
case 'MM':
|
|
11823
|
+
case 'DD':
|
|
11824
|
+
case 'HH':
|
|
11825
|
+
case 'hh':
|
|
11826
|
+
case 'mm':
|
|
11827
|
+
case 'ss':
|
|
11828
|
+
regex += '(\\d{1,2})';
|
|
11829
|
+
break;
|
|
11830
|
+
case 'M':
|
|
11831
|
+
case 'D':
|
|
11832
|
+
case 'H':
|
|
11833
|
+
case 'h':
|
|
11834
|
+
case 'm':
|
|
11835
|
+
case 's':
|
|
11836
|
+
regex += '(\\d{1,2})';
|
|
11837
|
+
break;
|
|
11838
|
+
case 'A':
|
|
11839
|
+
regex += '(AM|PM)';
|
|
11840
|
+
break;
|
|
11841
|
+
case 'a':
|
|
11842
|
+
regex += '(am|pm)';
|
|
11843
|
+
break;
|
|
11844
|
+
}
|
|
11845
|
+
i += token.length;
|
|
11846
|
+
}
|
|
11847
|
+
else {
|
|
11848
|
+
regex += escapeRegExp(one);
|
|
11849
|
+
i += 1;
|
|
11702
11850
|
}
|
|
11703
11851
|
}
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11852
|
+
const m = new RegExp('^' + regex + '$').exec(input);
|
|
11853
|
+
if (!m)
|
|
11854
|
+
return null;
|
|
11855
|
+
const out = {};
|
|
11856
|
+
tokenOrder.forEach((token, idx) => {
|
|
11857
|
+
const v = m[idx + 1];
|
|
11858
|
+
switch (token) {
|
|
11859
|
+
case 'YYYY':
|
|
11860
|
+
out.year = Number(v);
|
|
11861
|
+
break;
|
|
11862
|
+
case 'YY':
|
|
11863
|
+
out.year = 2000 + Number(v);
|
|
11864
|
+
break;
|
|
11865
|
+
case 'MM':
|
|
11866
|
+
case 'M':
|
|
11867
|
+
out.month = Number(v);
|
|
11868
|
+
break;
|
|
11869
|
+
case 'DD':
|
|
11870
|
+
case 'D':
|
|
11871
|
+
out.day = Number(v);
|
|
11872
|
+
break;
|
|
11873
|
+
case 'HH':
|
|
11874
|
+
case 'H':
|
|
11875
|
+
out.hours = Number(v);
|
|
11876
|
+
break;
|
|
11877
|
+
case 'hh':
|
|
11878
|
+
case 'h':
|
|
11879
|
+
out.hours = Number(v);
|
|
11880
|
+
break;
|
|
11881
|
+
case 'mm':
|
|
11882
|
+
case 'm':
|
|
11883
|
+
out.minutes = Number(v);
|
|
11884
|
+
break;
|
|
11885
|
+
case 'ss':
|
|
11886
|
+
case 's':
|
|
11887
|
+
out.seconds = Number(v);
|
|
11888
|
+
break;
|
|
11889
|
+
case 'A':
|
|
11890
|
+
out.meridiem = v.toLowerCase() === 'am' ? 'am' : 'pm';
|
|
11891
|
+
break;
|
|
11892
|
+
case 'a':
|
|
11893
|
+
out.meridiem = v === 'am' ? 'am' : 'pm';
|
|
11894
|
+
break;
|
|
11895
|
+
}
|
|
11896
|
+
});
|
|
11897
|
+
if (out.meridiem !== undefined && out.hours !== undefined) {
|
|
11898
|
+
if (out.meridiem === 'pm' && out.hours < 12)
|
|
11899
|
+
out.hours += 12;
|
|
11900
|
+
if (out.meridiem === 'am' && out.hours === 12)
|
|
11901
|
+
out.hours = 0;
|
|
11707
11902
|
}
|
|
11708
|
-
|
|
11709
|
-
return r;
|
|
11903
|
+
return out;
|
|
11710
11904
|
}
|
|
11711
11905
|
/**
|
|
11712
|
-
*
|
|
11713
|
-
*
|
|
11714
|
-
* of the month, where the week starts on `firstDayOfWeek`.
|
|
11906
|
+
* Locale-aware day name labels in calendar order (Sunday → Saturday).
|
|
11907
|
+
* Caller rotates by `firstDayOfWeek`.
|
|
11715
11908
|
*/
|
|
11716
|
-
function
|
|
11717
|
-
const
|
|
11718
|
-
|
|
11719
|
-
const
|
|
11720
|
-
const
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
cells.push({
|
|
11726
|
-
date,
|
|
11727
|
-
inCurrentMonth: date.getMonth() === monthAnchor.getMonth(),
|
|
11728
|
-
isToday: isSameDay(date, today),
|
|
11729
|
-
});
|
|
11909
|
+
function getDayNames(locale, length = 'short') {
|
|
11910
|
+
const fmt = new Intl.DateTimeFormat(locale, { weekday: length });
|
|
11911
|
+
// 2021-01-03 was a Sunday; use it as the anchor.
|
|
11912
|
+
const sunday = new Date(2021, 0, 3);
|
|
11913
|
+
const out = [];
|
|
11914
|
+
for (let i = 0; i < 7; i++) {
|
|
11915
|
+
const d = new Date(sunday);
|
|
11916
|
+
d.setDate(sunday.getDate() + i);
|
|
11917
|
+
out.push(fmt.format(d));
|
|
11730
11918
|
}
|
|
11731
|
-
return
|
|
11919
|
+
return out;
|
|
11920
|
+
}
|
|
11921
|
+
function getMonthNames(locale, length = 'long') {
|
|
11922
|
+
const fmt = new Intl.DateTimeFormat(locale, { month: length });
|
|
11923
|
+
const out = [];
|
|
11924
|
+
for (let i = 0; i < 12; i++) {
|
|
11925
|
+
out.push(fmt.format(new Date(2021, i, 1)));
|
|
11926
|
+
}
|
|
11927
|
+
return out;
|
|
11732
11928
|
}
|
|
11733
11929
|
|
|
11930
|
+
/**
|
|
11931
|
+
* Selects the calendar's focused cell, the one tab stop of whichever grid or
|
|
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.
|
|
11934
|
+
*/
|
|
11935
|
+
const CALENDAR_FOCUSED_CELL = '[role="grid"] [data-focused="true"], [role="listbox"] [data-focused-year="true"]';
|
|
11734
11936
|
const BASE_VIEW = {
|
|
11735
11937
|
day: 'days',
|
|
11736
11938
|
month: 'months',
|
|
@@ -11776,20 +11978,32 @@ function monthStep(key, col) {
|
|
|
11776
11978
|
}
|
|
11777
11979
|
}
|
|
11778
11980
|
/**
|
|
11779
|
-
*
|
|
11780
|
-
*
|
|
11781
|
-
*
|
|
11981
|
+
* Whether DOM focus should follow a grid's tab stop as it moves: focus is in
|
|
11982
|
+
* the grid, or fell to <body> while the grid held it last, as it does when
|
|
11983
|
+
* the focused cell leaves the page or becomes disabled. Focus is read in the
|
|
11984
|
+
* grid's own tree, so this holds inside a shadow root too.
|
|
11782
11985
|
*/
|
|
11783
|
-
function
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
11986
|
+
function focusFollows(grid, heldLast) {
|
|
11987
|
+
const active = getActiveElementInTree(grid);
|
|
11988
|
+
if (grid.contains(active))
|
|
11989
|
+
return true;
|
|
11990
|
+
return heldLast && active === grid.ownerDocument.body;
|
|
11991
|
+
}
|
|
11992
|
+
/**
|
|
11993
|
+
* The cell nearest index `i` that `disabled` leaves enabled, `i` itself when
|
|
11994
|
+
* it is. At equal distance the later cell wins. With every cell disabled it
|
|
11995
|
+
* stays `i`, as there is nothing to reach.
|
|
11996
|
+
*/
|
|
11997
|
+
function nearestEnabled(i, disabled) {
|
|
11998
|
+
if (!disabled[i])
|
|
11999
|
+
return i;
|
|
12000
|
+
for (let d = 1; d < disabled.length; d++) {
|
|
12001
|
+
if (i + d < disabled.length && !disabled[i + d])
|
|
12002
|
+
return i + d;
|
|
12003
|
+
if (i - d >= 0 && !disabled[i - d])
|
|
12004
|
+
return i - d;
|
|
11791
12005
|
}
|
|
11792
|
-
return
|
|
12006
|
+
return i;
|
|
11793
12007
|
}
|
|
11794
12008
|
/**
|
|
11795
12009
|
* The year list's keyboard when it is the selection surface: arrows move a
|
|
@@ -11815,7 +12029,7 @@ function yearStep(key, focusedYear, years) {
|
|
|
11815
12029
|
return null;
|
|
11816
12030
|
}
|
|
11817
12031
|
}
|
|
11818
|
-
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', }) => {
|
|
11819
12033
|
const gridRef = useRef(null);
|
|
11820
12034
|
const monthGridRef = useRef(null);
|
|
11821
12035
|
const yearGridRef = useRef(null);
|
|
@@ -11825,6 +12039,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11825
12039
|
const isDayGranularity = granularity === 'day';
|
|
11826
12040
|
const isYearGranularity = granularity === 'year';
|
|
11827
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]);
|
|
11828
12044
|
const computedDayNames = useMemo(() => {
|
|
11829
12045
|
if (dayNames && dayNames.length === 7)
|
|
11830
12046
|
return dayNames;
|
|
@@ -11861,6 +12077,33 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11861
12077
|
shouldDisableDate,
|
|
11862
12078
|
unselectableDates,
|
|
11863
12079
|
}), [min, max, shouldDisableDate, unselectableDates]);
|
|
12080
|
+
// The day grid's one tab stop. A disabled button can't take focus, so when
|
|
12081
|
+
// the focused day is disabled the stop moves to the nearest enabled day of
|
|
12082
|
+
// the month on show, and focusing it makes it the focused day. A nearby
|
|
12083
|
+
// month's day is left out, as focusing it would turn the grid to its month.
|
|
12084
|
+
const tabStopDay = useMemo(() => {
|
|
12085
|
+
const days = cells.filter(c => c.inCurrentMonth).map(c => c.date);
|
|
12086
|
+
const i = nearestEnabled(focusedDate.getDate() - 1, days.map(isDateUnselectable));
|
|
12087
|
+
return days[i];
|
|
12088
|
+
}, [cells, focusedDate, isDateUnselectable]);
|
|
12089
|
+
// Whether the grid on show holds focus, or held it last before focus went
|
|
12090
|
+
// nowhere: the cell that had it left the page or became disabled. A blur
|
|
12091
|
+
// that takes focus out of the grid, to nowhere included, is the user
|
|
12092
|
+
// leaving, so it clears this. A render that removes or disables the focused
|
|
12093
|
+
// cell sends no blur here: browsers fire none, or fire it inside React's
|
|
12094
|
+
// commit, where React delivers no events.
|
|
12095
|
+
const gridHeldFocusRef = useRef(false);
|
|
12096
|
+
const gridFocusHandlers = {
|
|
12097
|
+
onFocus: () => {
|
|
12098
|
+
gridHeldFocusRef.current = true;
|
|
12099
|
+
},
|
|
12100
|
+
onBlur: (e) => {
|
|
12101
|
+
const next = e.relatedTarget;
|
|
12102
|
+
if (!next || !e.currentTarget.contains(next)) {
|
|
12103
|
+
gridHeldFocusRef.current = false;
|
|
12104
|
+
}
|
|
12105
|
+
},
|
|
12106
|
+
};
|
|
11864
12107
|
// Step from `next` by `direction` days until we hit a selectable date,
|
|
11865
12108
|
// skipping past dates blocked by min/max, shouldDisableDate, or
|
|
11866
12109
|
// unselectableDates. If the whole searched range is disabled, focus stays
|
|
@@ -11868,7 +12111,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11868
12111
|
const moveFocus = useCallback((next, direction) => {
|
|
11869
12112
|
let candidate = next;
|
|
11870
12113
|
for (let i = 0; i < 366; i++) {
|
|
11871
|
-
if (
|
|
12114
|
+
if (candidate.getTime() < startOfDay(min).getTime())
|
|
11872
12115
|
return;
|
|
11873
12116
|
if (max && candidate.getTime() > startOfDay(max).getTime())
|
|
11874
12117
|
return;
|
|
@@ -11934,7 +12177,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11934
12177
|
let candidate = next;
|
|
11935
12178
|
for (let i = 0; i < 120; i++) {
|
|
11936
12179
|
const month = startOfMonth(candidate).getTime();
|
|
11937
|
-
if (
|
|
12180
|
+
if (month < startOfMonth(min).getTime())
|
|
11938
12181
|
return;
|
|
11939
12182
|
if (max && month > startOfMonth(max).getTime())
|
|
11940
12183
|
return;
|
|
@@ -11982,25 +12225,22 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11982
12225
|
const today = new Date();
|
|
11983
12226
|
const todayYear = today.getFullYear();
|
|
11984
12227
|
// The header steps a month at a time over the day grid and a year at a
|
|
11985
|
-
// time over the month grid, kept inside min/max.
|
|
11986
|
-
|
|
11987
|
-
|
|
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.
|
|
11988
12231
|
const prevAnchor = isDayGranularity
|
|
11989
|
-
?
|
|
12232
|
+
? addMonths(focusedDate, -1)
|
|
11990
12233
|
: clampDate(addYears(focusedDate, -1), min, max);
|
|
11991
12234
|
const nextAnchor = isDayGranularity
|
|
11992
|
-
?
|
|
12235
|
+
? addMonths(focusedDate, 1)
|
|
11993
12236
|
: clampDate(addYears(focusedDate, 1), min, max);
|
|
12237
|
+
const focusedMonth = startOfMonth(focusedDate).getTime();
|
|
11994
12238
|
const prevDisabled = isDayGranularity
|
|
11995
|
-
?
|
|
11996
|
-
|
|
11997
|
-
isSameMonth(focusedDate, min))
|
|
11998
|
-
: !!(min && focusedYear <= min.getFullYear());
|
|
12239
|
+
? focusedMonth <= startOfMonth(min).getTime()
|
|
12240
|
+
: focusedYear <= min.getFullYear();
|
|
11999
12241
|
const nextDisabled = isDayGranularity
|
|
12000
|
-
? !!
|
|
12001
|
-
|
|
12002
|
-
isSameMonth(focusedDate, max))
|
|
12003
|
-
: !!(max && focusedYear >= max.getFullYear());
|
|
12242
|
+
? !!max && focusedMonth >= startOfMonth(max).getTime()
|
|
12243
|
+
: !!max && focusedYear >= max.getFullYear();
|
|
12004
12244
|
const labelId = id ? `${id}-label` : undefined;
|
|
12005
12245
|
const monthLabel = isDayGranularity
|
|
12006
12246
|
? `${computedMonthNames[focusedDate.getMonth()]} ${focusedYear}`
|
|
@@ -12011,7 +12251,9 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12011
12251
|
const [mountYear] = useState(focusedYear);
|
|
12012
12252
|
const windowYear = isYearGranularity ? mountYear : focusedYear;
|
|
12013
12253
|
const yearList = useMemo(() => {
|
|
12014
|
-
const minYear =
|
|
12254
|
+
const minYear = minProp
|
|
12255
|
+
? min.getFullYear()
|
|
12256
|
+
: Math.max(FIRST_YEAR, windowYear - 100);
|
|
12015
12257
|
const maxYear = max ? max.getFullYear() : windowYear + 100;
|
|
12016
12258
|
const [lo, hi] = yearsRange ?? [minYear, maxYear];
|
|
12017
12259
|
const start = Math.max(lo, minYear);
|
|
@@ -12020,10 +12262,25 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12020
12262
|
for (let y = start; y <= end; y++)
|
|
12021
12263
|
out.push(y);
|
|
12022
12264
|
return out;
|
|
12023
|
-
}, [windowYear, min, max, yearsRange]);
|
|
12265
|
+
}, [windowYear, minProp, min, max, yearsRange]);
|
|
12024
12266
|
// Only the selection surface disables years; as navigation every listed
|
|
12025
12267
|
// year can be visited.
|
|
12026
12268
|
const disabledYears = useMemo(() => new Set(isYearGranularity ? yearList.filter(y => isYearUnselectable(y)) : []), [isYearGranularity, yearList, isYearUnselectable]);
|
|
12269
|
+
// The year list's one tab stop, after the month grid's: when the focused
|
|
12270
|
+
// year is disabled the stop moves to the nearest enabled year in the list,
|
|
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.
|
|
12275
|
+
const tabStopYear = useMemo(() => {
|
|
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
|
+
}
|
|
12282
|
+
return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
|
|
12283
|
+
}, [yearList, focusedYear, disabledYears, isYearGranularity]);
|
|
12027
12284
|
// Which of the focused year's months have no selectable day.
|
|
12028
12285
|
const disabledMonths = useMemo(() => granularity === 'month'
|
|
12029
12286
|
? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
|
|
@@ -12031,7 +12288,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12031
12288
|
// The month grid's one tab stop. A disabled button can't take focus, so
|
|
12032
12289
|
// when the focused month is disabled the stop moves to the nearest enabled
|
|
12033
12290
|
// month, and focusing it makes it the focused month.
|
|
12034
|
-
const tabStopMonth =
|
|
12291
|
+
const tabStopMonth = nearestEnabled(focusedDate.getMonth(), disabledMonths);
|
|
12035
12292
|
const handleYearSelect = useCallback((year) => {
|
|
12036
12293
|
const next = new Date(focusedDate);
|
|
12037
12294
|
next.setFullYear(year);
|
|
@@ -12073,24 +12330,37 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12073
12330
|
e.preventDefault();
|
|
12074
12331
|
moveYearFocus(step[0], step[1]);
|
|
12075
12332
|
}, [isYearGranularity, baseView, focusedYear, yearList, moveYearFocus]);
|
|
12333
|
+
// The day or month grid takes focus as the popover opens on it, or comes
|
|
12334
|
+
// back to it from the year list.
|
|
12335
|
+
useEffect(() => {
|
|
12336
|
+
if (!autoFocusCell)
|
|
12337
|
+
return;
|
|
12338
|
+
const grid = view === 'days'
|
|
12339
|
+
? gridRef.current
|
|
12340
|
+
: view === 'months'
|
|
12341
|
+
? monthGridRef.current
|
|
12342
|
+
: null;
|
|
12343
|
+
grid?.querySelector('[data-focused="true"]')?.focus();
|
|
12344
|
+
}, [autoFocusCell, view]);
|
|
12345
|
+
// From then on focus follows the tab stop only while the grid has it, or
|
|
12346
|
+
// has just lost it, so focus on a header button or a time wheel stays put.
|
|
12347
|
+
const tabStopDayTime = tabStopDay.getTime();
|
|
12076
12348
|
useEffect(() => {
|
|
12077
|
-
|
|
12349
|
+
const grid = gridRef.current;
|
|
12350
|
+
if (view !== 'days' || !grid)
|
|
12078
12351
|
return;
|
|
12079
|
-
if (!
|
|
12352
|
+
if (!focusFollows(grid, gridHeldFocusRef.current))
|
|
12080
12353
|
return;
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
}, [autoFocusCell, focusedDate, view]);
|
|
12084
|
-
// The month grid takes focus when the popover asks for it, and keeps it on
|
|
12085
|
-
// the focused month while the keyboard moves through the grid.
|
|
12354
|
+
grid.querySelector('[data-focused="true"]')?.focus();
|
|
12355
|
+
}, [tabStopDayTime, view]);
|
|
12086
12356
|
useEffect(() => {
|
|
12087
12357
|
const grid = monthGridRef.current;
|
|
12088
12358
|
if (view !== 'months' || !grid)
|
|
12089
12359
|
return;
|
|
12090
|
-
if (!
|
|
12360
|
+
if (!focusFollows(grid, gridHeldFocusRef.current))
|
|
12091
12361
|
return;
|
|
12092
12362
|
grid.querySelector('[data-focused="true"]')?.focus();
|
|
12093
|
-
}, [
|
|
12363
|
+
}, [focusedYear, tabStopMonth, view]);
|
|
12094
12364
|
// When the year view opens as navigation, scroll the focused year into
|
|
12095
12365
|
// view.
|
|
12096
12366
|
useEffect(() => {
|
|
@@ -12124,15 +12394,15 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12124
12394
|
const list = yearGridRef.current;
|
|
12125
12395
|
if (!isYearGranularity || !list)
|
|
12126
12396
|
return;
|
|
12127
|
-
if (!list.contains(
|
|
12397
|
+
if (!list.contains(getActiveElementInTree(list)))
|
|
12128
12398
|
return;
|
|
12129
12399
|
list.querySelector('[data-focused-year="true"]')?.focus();
|
|
12130
12400
|
}, [isYearGranularity, focusedYear]);
|
|
12131
12401
|
const navArrow = (d) => (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: jsx("path", { d: d, fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
12132
|
-
return (jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => setView(v => (v === 'years' ? baseView : 'years')), children: [jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxs("div", { className: navGroupClass, children: [jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxs(Fragment, { children: [jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsx("div", { className: dayNameClass, children: name }, i))) }), jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, children: cells.map(cell => {
|
|
12402
|
+
return (jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => setView(v => (v === 'years' ? baseView : 'years')), children: [jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxs("div", { className: navGroupClass, children: [jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxs(Fragment, { children: [jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsx("div", { className: dayNameClass, children: name }, i))) }), jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
|
|
12133
12403
|
const disabled = isDateUnselectable(cell.date);
|
|
12134
12404
|
const isSelected = !!value && isSameDay(value, cell.date);
|
|
12135
|
-
const isFocused = isSameDay(cell.date,
|
|
12405
|
+
const isFocused = isSameDay(cell.date, tabStopDay);
|
|
12136
12406
|
const otherMonth = !cell.inCurrentMonth;
|
|
12137
12407
|
const cellClass = prefixedClassNames(classPrefix, 'dateinput-cell', {
|
|
12138
12408
|
'is-selected': isSelected,
|
|
@@ -12141,13 +12411,20 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12141
12411
|
'is-other-month': otherMonth,
|
|
12142
12412
|
});
|
|
12143
12413
|
const display = !otherMonth || nearbyMonthDays;
|
|
12144
|
-
return (jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass,
|
|
12414
|
+
return (jsx("button", { type: "button", role: "gridcell", tabIndex: isFocused ? 0 : -1, "aria-selected": isSelected, "aria-disabled": disabled, "aria-current": cell.isToday ? 'date' : undefined, "data-focused": isFocused ? 'true' : undefined, disabled: disabled || !display, className: cellClass, onFocus: () => {
|
|
12415
|
+
// Reached by Tab while the focused day is disabled, or
|
|
12416
|
+
// by pointer: keys move on from here. A nearby month's
|
|
12417
|
+
// day waits for its click, which turns the grid.
|
|
12418
|
+
if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
|
|
12419
|
+
onFocusedDateChange(cell.date);
|
|
12420
|
+
}
|
|
12421
|
+
}, onClick: () => {
|
|
12145
12422
|
if (disabled)
|
|
12146
12423
|
return;
|
|
12147
12424
|
onFocusedDateChange(cell.date);
|
|
12148
12425
|
onSelect(cell.date);
|
|
12149
12426
|
}, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
|
|
12150
|
-
}) })] })), view === 'months' && (jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, children: MONTH_ROWS.map(row => (jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
|
|
12427
|
+
}) })] })), view === 'months' && (jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
|
|
12151
12428
|
const date = makeDate(focusedYear, month);
|
|
12152
12429
|
const disabled = disabledMonths[month];
|
|
12153
12430
|
const isSelected = !!value && isSameMonth(value, date);
|
|
@@ -12169,7 +12446,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12169
12446
|
onSelect(date);
|
|
12170
12447
|
}, children: monthCellNames[month] }, month));
|
|
12171
12448
|
}) }, row[0]))) })), view === 'years' && (jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
|
|
12172
|
-
const isFocused = year ===
|
|
12449
|
+
const isFocused = year === tabStopYear;
|
|
12173
12450
|
const isToday = year === todayYear;
|
|
12174
12451
|
// As navigation the focused year is the current one. As the
|
|
12175
12452
|
// selection surface the value's year is selected and focus roams.
|
|
@@ -12182,7 +12459,13 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12182
12459
|
'is-today': isToday,
|
|
12183
12460
|
'is-disabled': disabled,
|
|
12184
12461
|
});
|
|
12185
|
-
return (jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: isFocused ? 0 : -1, disabled: disabled, className: cellCls,
|
|
12462
|
+
return (jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: isFocused ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
|
|
12463
|
+
// Reached by Tab while the focused year is disabled, or by
|
|
12464
|
+
// pointer: keys move on from here.
|
|
12465
|
+
if (isYearGranularity && year !== focusedYear) {
|
|
12466
|
+
onFocusedDateChange(clampDate(makeDate(year), min, max));
|
|
12467
|
+
}
|
|
12468
|
+
}, onClick: () => isYearGranularity
|
|
12186
12469
|
? handleYearPick(year)
|
|
12187
12470
|
: handleYearSelect(year), children: year }, year));
|
|
12188
12471
|
}) }))] }));
|
|
@@ -12190,12 +12473,25 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12190
12473
|
Calendar.displayName = 'Calendar';
|
|
12191
12474
|
|
|
12192
12475
|
const isBrowser = typeof window !== 'undefined';
|
|
12193
|
-
const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'bottom-left', appendToBody = false, className, trapFocus = true, closeOnClickOutside = true, closeOnEscape = true, ariaLabel, ariaLabelledBy, role = 'dialog', id, restoreFocusRef, }) => {
|
|
12476
|
+
const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'bottom-left', appendToBody = false, className, trapFocus = true, closeOnClickOutside = true, closeOnEscape = true, ariaLabel, ariaLabelledBy, role = 'dialog', id, restoreFocusRef, initialFocusSelector, }) => {
|
|
12194
12477
|
const panelRef = useRef(null);
|
|
12478
|
+
// Read as the trap turns on, once the panel's content has rendered.
|
|
12479
|
+
const initialFocusRef = useMemo(() => initialFocusSelector === undefined
|
|
12480
|
+
? undefined
|
|
12481
|
+
: {
|
|
12482
|
+
get current() {
|
|
12483
|
+
return (panelRef.current?.querySelector(initialFocusSelector) ?? null);
|
|
12484
|
+
},
|
|
12485
|
+
}, [initialFocusSelector]);
|
|
12195
12486
|
const resolved = useAnchoredPosition(anchorRef, panelRef, {
|
|
12196
12487
|
active: isOpen,
|
|
12197
12488
|
position,
|
|
12198
12489
|
fixed: appendToBody,
|
|
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,
|
|
12494
|
+
styledGap: marginGap,
|
|
12199
12495
|
});
|
|
12200
12496
|
useEffect(() => {
|
|
12201
12497
|
if (!isOpen || !closeOnClickOutside || !isBrowser)
|
|
@@ -12229,6 +12525,7 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
|
|
|
12229
12525
|
}, [isOpen, closeOnEscape, onClose]);
|
|
12230
12526
|
useFocusTrap(panelRef, {
|
|
12231
12527
|
active: isOpen && trapFocus,
|
|
12528
|
+
initialFocusRef,
|
|
12232
12529
|
restoreFocus: restoreFocusRef ?? true,
|
|
12233
12530
|
});
|
|
12234
12531
|
const panelClass = usePrefixedClassNames('picker-popover', {
|
|
@@ -12408,8 +12705,11 @@ function buildSegmentMap(format) {
|
|
|
12408
12705
|
// ---------------------------------------------------------------------------
|
|
12409
12706
|
const wrap$1 = (n, max) => ((n % max) + max) % max;
|
|
12410
12707
|
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
|
12411
|
-
/**
|
|
12412
|
-
|
|
12708
|
+
/**
|
|
12709
|
+
* Number of days in the month of `d` (handles leap Februarys). Built on
|
|
12710
|
+
* `makeDate`, so a year below 100 keeps its own leap rule.
|
|
12711
|
+
*/
|
|
12712
|
+
const daysInMonth = (d) => makeDate(d.getFullYear(), d.getMonth() + 1, 0).getDate();
|
|
12413
12713
|
/**
|
|
12414
12714
|
* Returns a new Date with the given segment incremented (`delta` = +1 or -1).
|
|
12415
12715
|
* Wraps at segment boundaries: year is unbounded, month 0↔11 (in place, no
|
|
@@ -12636,33 +12936,35 @@ function useSegmentedEntry(params) {
|
|
|
12636
12936
|
// within the same event tick.
|
|
12637
12937
|
const typedDigitsRef = useRef('');
|
|
12638
12938
|
// The text focus seeded an empty picker with, until the user edits it or
|
|
12639
|
-
// leaves.
|
|
12939
|
+
// leaves.
|
|
12640
12940
|
const seedRef = useRef(null);
|
|
12641
|
-
// Whether the
|
|
12642
|
-
//
|
|
12643
|
-
|
|
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]);
|
|
12644
12948
|
// A seed the popover never turned into a value closes with it, wherever
|
|
12645
12949
|
// focus goes, so a dismissed empty field stays empty.
|
|
12646
12950
|
//
|
|
12647
12951
|
// As the popover closes, its focus trap hands focus back to the input. That
|
|
12648
12952
|
// focus is not the user arriving: it must not open the popover again under
|
|
12649
|
-
// `openOnFocus`, or seed an empty picker
|
|
12650
|
-
//
|
|
12651
|
-
//
|
|
12652
|
-
//
|
|
12653
|
-
// 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.
|
|
12654
12957
|
const closingRef = useRef(false);
|
|
12655
12958
|
const wasOpenRef = useRef(isOpen);
|
|
12656
12959
|
useLayoutEffect(() => {
|
|
12657
12960
|
if (wasOpenRef.current && !isOpen) {
|
|
12658
12961
|
closingRef.current = true;
|
|
12659
|
-
if (!value &&
|
|
12962
|
+
if (!value && isUntouched(text))
|
|
12660
12963
|
setText('');
|
|
12661
|
-
}
|
|
12662
12964
|
seedRef.current = null;
|
|
12663
12965
|
}
|
|
12664
12966
|
wasOpenRef.current = isOpen;
|
|
12665
|
-
}, [isOpen, value, text, setText]);
|
|
12967
|
+
}, [isOpen, value, isUntouched, text, setText]);
|
|
12666
12968
|
useEffect(() => {
|
|
12667
12969
|
closingRef.current = false;
|
|
12668
12970
|
}, [isOpen]);
|
|
@@ -12740,13 +13042,18 @@ function useSegmentedEntry(params) {
|
|
|
12740
13042
|
}
|
|
12741
13043
|
setActiveSegmentIdx(null);
|
|
12742
13044
|
typedDigitsRef.current = '';
|
|
12743
|
-
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
//
|
|
12748
|
-
//
|
|
12749
|
-
|
|
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('');
|
|
12750
13057
|
onBlur?.(e);
|
|
12751
13058
|
return;
|
|
12752
13059
|
}
|
|
@@ -12760,6 +13067,8 @@ function useSegmentedEntry(params) {
|
|
|
12760
13067
|
onBlur?.(e);
|
|
12761
13068
|
}, [
|
|
12762
13069
|
containerRef,
|
|
13070
|
+
isUntouched,
|
|
13071
|
+
isOpen,
|
|
12763
13072
|
tryParse,
|
|
12764
13073
|
text,
|
|
12765
13074
|
isAllowed,
|
|
@@ -12773,7 +13082,6 @@ function useSegmentedEntry(params) {
|
|
|
12773
13082
|
]);
|
|
12774
13083
|
const handleFocus = useCallback((e) => {
|
|
12775
13084
|
const handedBack = closingRef.current;
|
|
12776
|
-
handedBackRef.current = handedBack;
|
|
12777
13085
|
if (!handedBack && openOnFocus && popover && !disabled && !readOnly) {
|
|
12778
13086
|
setOpen(true);
|
|
12779
13087
|
}
|
|
@@ -12941,12 +13249,20 @@ function useSegmentedEntry(params) {
|
|
|
12941
13249
|
}
|
|
12942
13250
|
if (e.key === 'Enter') {
|
|
12943
13251
|
e.preventDefault();
|
|
12944
|
-
|
|
12945
|
-
|
|
12946
|
-
|
|
13252
|
+
// Untouched text has nothing to commit, as on leaving, but Enter
|
|
13253
|
+
// still confirms what the field shows.
|
|
13254
|
+
if (isUntouched(text)) {
|
|
12947
13255
|
if (closeOnSelect)
|
|
12948
13256
|
setOpen(false);
|
|
12949
13257
|
}
|
|
13258
|
+
else {
|
|
13259
|
+
const parsed = tryParse(text);
|
|
13260
|
+
if (parsed && isAllowed(parsed)) {
|
|
13261
|
+
commitValue(parsed);
|
|
13262
|
+
if (closeOnSelect)
|
|
13263
|
+
setOpen(false);
|
|
13264
|
+
}
|
|
13265
|
+
}
|
|
12950
13266
|
}
|
|
12951
13267
|
onKeyDown?.(e);
|
|
12952
13268
|
}, [
|
|
@@ -12960,6 +13276,7 @@ function useSegmentedEntry(params) {
|
|
|
12960
13276
|
setOpen,
|
|
12961
13277
|
closeOnSelect,
|
|
12962
13278
|
popover,
|
|
13279
|
+
isUntouched,
|
|
12963
13280
|
tryParse,
|
|
12964
13281
|
text,
|
|
12965
13282
|
isAllowed,
|
|
@@ -13008,25 +13325,31 @@ const pad2$1 = (n) => String(n).padStart(2, '0');
|
|
|
13008
13325
|
* The value as the native input and the hidden form input carry it:
|
|
13009
13326
|
* `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
|
|
13010
13327
|
* `<input type="month">` use. The year is padded to four digits, as those
|
|
13011
|
-
* 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.
|
|
13012
13330
|
*/
|
|
13013
13331
|
const toIsoValue = (d, granularity) => {
|
|
13332
|
+
if (d.getFullYear() < FIRST_YEAR)
|
|
13333
|
+
return '';
|
|
13014
13334
|
const year = String(d.getFullYear()).padStart(4, '0');
|
|
13015
13335
|
if (granularity === 'year')
|
|
13016
13336
|
return year;
|
|
13017
13337
|
const month = `${year}-${pad2$1(d.getMonth() + 1)}`;
|
|
13018
13338
|
return granularity === 'month' ? month : `${month}-${pad2$1(d.getDate())}`;
|
|
13019
13339
|
};
|
|
13020
|
-
/**
|
|
13340
|
+
/**
|
|
13341
|
+
* Read a native `type="date"` or `type="month"` value back into a Date. HTML
|
|
13342
|
+
* allows a year of four or more digits, as `toIsoValue` writes one past 9999.
|
|
13343
|
+
*/
|
|
13021
13344
|
const fromIsoValue = (s, granularity) => {
|
|
13022
13345
|
if (granularity === 'month') {
|
|
13023
|
-
const m = /^(\d{4})-(\d{2})$/.exec(s);
|
|
13346
|
+
const m = /^(\d{4,})-(\d{2})$/.exec(s);
|
|
13024
13347
|
return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
|
|
13025
13348
|
}
|
|
13026
|
-
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
13349
|
+
const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
|
|
13027
13350
|
if (!m)
|
|
13028
13351
|
return null;
|
|
13029
|
-
return
|
|
13352
|
+
return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
13030
13353
|
};
|
|
13031
13354
|
const DEFAULT_FORMATS = {
|
|
13032
13355
|
day: DEFAULT_DATE_FORMAT,
|
|
@@ -13048,14 +13371,19 @@ const SKIPPED_SEGMENTS = {
|
|
|
13048
13371
|
* @returns {JSX.Element}
|
|
13049
13372
|
*/
|
|
13050
13373
|
const DateInputBase = forwardRef((props, ref) => {
|
|
13051
|
-
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;
|
|
13052
13378
|
const t = mergeLabels(labels);
|
|
13053
13379
|
const isDayGranularity = granularity === 'day';
|
|
13054
13380
|
const resolvedFormat = format ?? DEFAULT_FORMATS[granularity];
|
|
13055
13381
|
const isControlled = controlledValue !== undefined;
|
|
13056
13382
|
const [internalValue, setInternalValue] = useState(defaultValue ?? null);
|
|
13057
13383
|
const value = isControlled ? (controlledValue ?? null) : internalValue;
|
|
13058
|
-
|
|
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),
|
|
13059
13387
|
// intentionally only on mount: keep focusedDate stable until value/open change
|
|
13060
13388
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
13061
13389
|
[]);
|
|
@@ -13063,8 +13391,8 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13063
13391
|
// Re-clamp focusedDate if min/max change after mount so the focused cell
|
|
13064
13392
|
// never disappears outside the displayable range.
|
|
13065
13393
|
useEffect(() => {
|
|
13066
|
-
setFocusedDate(prev => clampDate(prev,
|
|
13067
|
-
}, [
|
|
13394
|
+
setFocusedDate(prev => clampDate(prev, lowerBound, max));
|
|
13395
|
+
}, [lowerBound, max]);
|
|
13068
13396
|
const [open, setOpenState] = useState(false);
|
|
13069
13397
|
const [text, setText] = useState(value ? formatDate(value, resolvedFormat, locale) : '');
|
|
13070
13398
|
const inputRef = useRef(null);
|
|
@@ -13090,16 +13418,20 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13090
13418
|
});
|
|
13091
13419
|
const containerClass = usePrefixedClassNames('dateinput-container');
|
|
13092
13420
|
const triggerClass = usePrefixedClassNames('dateinput-trigger');
|
|
13421
|
+
// What the latest request asked for, so a second request in the same
|
|
13422
|
+
// event sees it before React renders. The callbacks fire here rather than
|
|
13423
|
+
// in a state updater, which StrictMode calls twice, and after the state
|
|
13424
|
+
// is set, so a request a callback makes is the one that lands last.
|
|
13425
|
+
const requestedOpenRef = useRef(false);
|
|
13093
13426
|
const setOpen = useCallback((next) => {
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
});
|
|
13427
|
+
if (requestedOpenRef.current === next)
|
|
13428
|
+
return;
|
|
13429
|
+
requestedOpenRef.current = next;
|
|
13430
|
+
setOpenState(next);
|
|
13431
|
+
if (next)
|
|
13432
|
+
onOpen?.();
|
|
13433
|
+
else
|
|
13434
|
+
onClose?.();
|
|
13103
13435
|
}, [onOpen, onClose]);
|
|
13104
13436
|
// Sync displayed text with value when value changes externally.
|
|
13105
13437
|
useEffect(() => {
|
|
@@ -13118,7 +13450,7 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13118
13450
|
}, [isControlled, onChange, isDayGranularity, granularity]);
|
|
13119
13451
|
// At month or year granularity the bounds widen to the periods holding
|
|
13120
13452
|
// them, so the month or year containing `min` stays reachable.
|
|
13121
|
-
const periodMin = useMemo(() =>
|
|
13453
|
+
const periodMin = useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
|
|
13122
13454
|
const periodMax = useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
|
|
13123
13455
|
const handleSelect = useCallback((d) => {
|
|
13124
13456
|
if (!isWithin(d, periodMin, periodMax))
|
|
@@ -13169,7 +13501,7 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13169
13501
|
]);
|
|
13170
13502
|
// Typing, and the re-parse on blur or Enter, that lands in the period the
|
|
13171
13503
|
// value already holds commits nothing, so a `value` elsewhere in its month
|
|
13172
|
-
// or year
|
|
13504
|
+
// or year changes only when the user picks or types a different period.
|
|
13173
13505
|
const commitTyped = useCallback((next) => {
|
|
13174
13506
|
if (next &&
|
|
13175
13507
|
value &&
|
|
@@ -13235,7 +13567,7 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13235
13567
|
? fromIsoValue(e.target.value, granularity)
|
|
13236
13568
|
: null;
|
|
13237
13569
|
commitValue(parsed);
|
|
13238
|
-
}, 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 }));
|
|
13239
13571
|
}
|
|
13240
13572
|
const chooseLabel = granularity === 'month'
|
|
13241
13573
|
? t.chooseMonth
|
|
@@ -13244,14 +13576,18 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13244
13576
|
: t.chooseDate;
|
|
13245
13577
|
const calendar = (jsx(Calendar
|
|
13246
13578
|
// A new granularity starts the calendar over on its own view.
|
|
13247
|
-
, { 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));
|
|
13248
13584
|
if (inline) {
|
|
13249
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 }))] }));
|
|
13250
13586
|
}
|
|
13251
13587
|
return (jsxs("div", { ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), ...cleanRest, children: [jsx("input", { ref: combinedRef, type: "text", role: "combobox", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": popoverId, autoComplete: "off", className: inputClass, value: text, placeholder: placeholder, disabled: disabled, readOnly: inputReadOnlyAttr, name: name, form: form, required: required, id: id, ...inputHandlers }), popover && triggerIcon && (jsx("button", { type: "button", className: triggerClass, onClick: () => {
|
|
13252
13588
|
if (canOpen)
|
|
13253
13589
|
setOpen(!open);
|
|
13254
|
-
}, disabled: !canOpen, "aria-label": chooseLabel, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsx(PickerPopover, { isOpen: open, onClose: handlePopoverClose, anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: chooseLabel, id: popoverId, restoreFocusRef: inputRef, children: calendar }))] }));
|
|
13590
|
+
}, disabled: !canOpen, "aria-label": chooseLabel, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsx(PickerPopover, { isOpen: open, onClose: handlePopoverClose, anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: chooseLabel, id: popoverId, restoreFocusRef: inputRef, initialFocusSelector: CALENDAR_FOCUSED_CELL, children: calendar }))] }));
|
|
13255
13591
|
});
|
|
13256
13592
|
DateInputBase.displayName = 'DateInputBase';
|
|
13257
13593
|
|
|
@@ -13275,26 +13611,34 @@ DateInputBase.displayName = 'DateInputBase';
|
|
|
13275
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) => {
|
|
13276
13612
|
const insideField = useInsideField();
|
|
13277
13613
|
const insideControl = useInsideControl();
|
|
13614
|
+
const ownField = rendersOwnField('DateInput', {
|
|
13615
|
+
insideField,
|
|
13616
|
+
insideControl,
|
|
13617
|
+
label,
|
|
13618
|
+
message,
|
|
13619
|
+
horizontal,
|
|
13620
|
+
fieldClassName,
|
|
13621
|
+
});
|
|
13278
13622
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
13279
13623
|
label,
|
|
13280
13624
|
id: baseProps.id,
|
|
13281
13625
|
labelProps,
|
|
13282
13626
|
// Inline mode renders a bare calendar with no input to label.
|
|
13283
|
-
rendersLabel:
|
|
13627
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
13284
13628
|
});
|
|
13285
13629
|
const helpClass = usePrefixedClassNames('help', {
|
|
13286
13630
|
[`is-${messageColor}`]: !!messageColor,
|
|
13287
13631
|
});
|
|
13288
|
-
// The
|
|
13289
|
-
//
|
|
13290
|
-
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 }));
|
|
13291
13635
|
// Inline mode renders a bare calendar with no input, so the Control's
|
|
13292
13636
|
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
13293
13637
|
if (!insideControl && !baseProps.inline) {
|
|
13294
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 }));
|
|
13295
13639
|
}
|
|
13296
13640
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
13297
|
-
if (
|
|
13641
|
+
if (ownField) {
|
|
13298
13642
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
13299
13643
|
}
|
|
13300
13644
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -14168,7 +14512,10 @@ const fromIsoTime = (s) => {
|
|
|
14168
14512
|
* @returns {JSX.Element}
|
|
14169
14513
|
*/
|
|
14170
14514
|
const TimeInputBase = forwardRef((props, ref) => {
|
|
14171
|
-
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;
|
|
14172
14519
|
// Platform-appropriate feedback routing: when `haptics` is opted in and
|
|
14173
14520
|
// the runtime has no Vibration API (notably iOS Safari), enable the
|
|
14174
14521
|
// audio thunk so the user still gets *some* tactile cue. When vibrate
|
|
@@ -14224,23 +14571,29 @@ const TimeInputBase = forwardRef((props, ref) => {
|
|
|
14224
14571
|
const mobileFooterClass = usePrefixedClassNames('timeinput-footer-mobile');
|
|
14225
14572
|
const mobileFooterResetClass = usePrefixedClassNames('timeinput-footer-reset');
|
|
14226
14573
|
const mobileFooterDoneClass = usePrefixedClassNames('timeinput-footer-done');
|
|
14574
|
+
// What the latest request asked for, so a second request in the same
|
|
14575
|
+
// event sees it before React renders. The callbacks fire here rather than
|
|
14576
|
+
// in a state updater, which StrictMode calls twice, and after the state
|
|
14577
|
+
// is set, so a request a callback makes is the one that lands last.
|
|
14578
|
+
const requestedOpenRef = useRef(false);
|
|
14227
14579
|
const setOpen = useCallback((next) => {
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
});
|
|
14580
|
+
if (requestedOpenRef.current === next)
|
|
14581
|
+
return;
|
|
14582
|
+
requestedOpenRef.current = next;
|
|
14583
|
+
setOpenState(next);
|
|
14584
|
+
if (next) {
|
|
14585
|
+
valueAtOpenRef.current = value;
|
|
14586
|
+
onOpen?.();
|
|
14587
|
+
}
|
|
14588
|
+
else {
|
|
14589
|
+
onClose?.();
|
|
14590
|
+
}
|
|
14240
14591
|
}, [onOpen, onClose, value]);
|
|
14241
14592
|
useEffect(() => {
|
|
14242
14593
|
setText(value ? formatTime(value, defaultFormat, locale) : '');
|
|
14243
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]);
|
|
14244
14597
|
const commitValue = useCallback((next) => {
|
|
14245
14598
|
if (!isControlled)
|
|
14246
14599
|
setInternalValue(next);
|
|
@@ -14249,10 +14602,10 @@ const TimeInputBase = forwardRef((props, ref) => {
|
|
|
14249
14602
|
const handleSpinnerChange = useCallback((parts) => {
|
|
14250
14603
|
const base = value ?? new Date();
|
|
14251
14604
|
const next = setTimeOfDay(base, parts);
|
|
14252
|
-
if (!isWithin(next,
|
|
14605
|
+
if (!isWithin(next, lowerBound, max))
|
|
14253
14606
|
return;
|
|
14254
14607
|
commitValue(next);
|
|
14255
|
-
}, [value,
|
|
14608
|
+
}, [value, lowerBound, max, commitValue]);
|
|
14256
14609
|
const tryParse = useCallback((s) => {
|
|
14257
14610
|
const trimmed = s.trim();
|
|
14258
14611
|
if (!trimmed)
|
|
@@ -14284,7 +14637,7 @@ const TimeInputBase = forwardRef((props, ref) => {
|
|
|
14284
14637
|
setText,
|
|
14285
14638
|
makeBaseDate,
|
|
14286
14639
|
locale,
|
|
14287
|
-
min,
|
|
14640
|
+
min: lowerBound,
|
|
14288
14641
|
max,
|
|
14289
14642
|
isBlocked: unselectableTimes,
|
|
14290
14643
|
disabled,
|
|
@@ -14366,34 +14719,47 @@ TimeInputBase.displayName = 'TimeInputBase';
|
|
|
14366
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) => {
|
|
14367
14720
|
const insideField = useInsideField();
|
|
14368
14721
|
const insideControl = useInsideControl();
|
|
14722
|
+
const ownField = rendersOwnField('TimeInput', {
|
|
14723
|
+
insideField,
|
|
14724
|
+
insideControl,
|
|
14725
|
+
label,
|
|
14726
|
+
message,
|
|
14727
|
+
horizontal,
|
|
14728
|
+
fieldClassName,
|
|
14729
|
+
});
|
|
14369
14730
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
14370
14731
|
label,
|
|
14371
14732
|
id: baseProps.id,
|
|
14372
14733
|
labelProps,
|
|
14373
14734
|
// Inline mode renders a bare panel with no input to label.
|
|
14374
|
-
rendersLabel:
|
|
14735
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
14375
14736
|
});
|
|
14376
14737
|
const helpClass = usePrefixedClassNames('help', {
|
|
14377
14738
|
[`is-${messageColor}`]: !!messageColor,
|
|
14378
14739
|
});
|
|
14379
|
-
// The
|
|
14380
|
-
//
|
|
14381
|
-
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 }));
|
|
14382
14743
|
// Inline mode renders a bare panel with no input, so the Control's
|
|
14383
14744
|
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
14384
14745
|
if (!insideControl && !baseProps.inline) {
|
|
14385
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 }));
|
|
14386
14747
|
}
|
|
14387
14748
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
14388
|
-
if (
|
|
14749
|
+
if (ownField) {
|
|
14389
14750
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
14390
14751
|
}
|
|
14391
14752
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
14392
14753
|
});
|
|
14393
14754
|
TimeInput.displayName = 'TimeInput';
|
|
14394
14755
|
|
|
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.
|
|
14395
14759
|
const toIsoDateTime = (d, withSeconds) => {
|
|
14396
|
-
|
|
14760
|
+
if (d.getFullYear() < FIRST_YEAR)
|
|
14761
|
+
return '';
|
|
14762
|
+
const yyyy = String(d.getFullYear()).padStart(4, '0');
|
|
14397
14763
|
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
|
14398
14764
|
const dd = String(d.getDate()).padStart(2, '0');
|
|
14399
14765
|
const hh = String(d.getHours()).padStart(2, '0');
|
|
@@ -14406,10 +14772,13 @@ const toIsoDateTime = (d, withSeconds) => {
|
|
|
14406
14772
|
const fromIsoDateTime = (s) => {
|
|
14407
14773
|
// The HTML datetime-local value may carry fractional seconds (the spec
|
|
14408
14774
|
// allows them and some engines normalize to `:ss.sss`); accept and drop.
|
|
14409
|
-
|
|
14775
|
+
// Its year is four or more digits, as `toIsoDateTime` writes one past 9999.
|
|
14776
|
+
const m = /^(\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?$/.exec(s);
|
|
14410
14777
|
if (!m)
|
|
14411
14778
|
return null;
|
|
14412
|
-
|
|
14779
|
+
const d = makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
14780
|
+
d.setHours(Number(m[4]), Number(m[5]), m[6] ? Number(m[6]) : 0, 0);
|
|
14781
|
+
return d;
|
|
14413
14782
|
};
|
|
14414
14783
|
/**
|
|
14415
14784
|
* Raw DateTimeInput — combined input + calendar + time spinner without
|
|
@@ -14422,7 +14791,10 @@ const fromIsoDateTime = (s) => {
|
|
|
14422
14791
|
const DateTimeInputBase = forwardRef((props, ref) => {
|
|
14423
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,
|
|
14424
14793
|
// Step between visible wheel values; 1 gives the iOS-style every-minute wheel.
|
|
14425
|
-
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;
|
|
14426
14798
|
const t = mergeLabels(labels);
|
|
14427
14799
|
// Platform-appropriate feedback routing for the time wheel: on iOS (no
|
|
14428
14800
|
// navigator.vibrate) the audio thunk fills in; on Android the real haptic
|
|
@@ -14432,14 +14804,16 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14432
14804
|
const isControlled = controlledValue !== undefined;
|
|
14433
14805
|
const [internalValue, setInternalValue] = useState(defaultValue ?? null);
|
|
14434
14806
|
const value = isControlled ? (controlledValue ?? null) : internalValue;
|
|
14435
|
-
|
|
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),
|
|
14436
14810
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14437
14811
|
[]);
|
|
14438
14812
|
const [focusedDate, setFocusedDate] = useState(initialFocused);
|
|
14439
14813
|
// Re-clamp focusedDate when min/max change so the focused cell stays valid.
|
|
14440
14814
|
useEffect(() => {
|
|
14441
|
-
setFocusedDate(prev => clampDate(prev,
|
|
14442
|
-
}, [
|
|
14815
|
+
setFocusedDate(prev => clampDate(prev, lowerBound, max));
|
|
14816
|
+
}, [lowerBound, max]);
|
|
14443
14817
|
const defaultFormat = format ??
|
|
14444
14818
|
(hourFormat === '12'
|
|
14445
14819
|
? enableSeconds
|
|
@@ -14496,19 +14870,23 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14496
14870
|
const footerResetClass = usePrefixedClassNames('datetimeinput-footer-reset');
|
|
14497
14871
|
const footerDoneClass = usePrefixedClassNames('datetimeinput-footer-done');
|
|
14498
14872
|
const footerTimePillClass = usePrefixedClassNames('datetimeinput-footer-time-pill');
|
|
14873
|
+
// What the latest request asked for, so a second request in the same event
|
|
14874
|
+
// sees it before React renders. The callbacks fire here rather than in a
|
|
14875
|
+
// state updater, which StrictMode calls twice, and after the state is set,
|
|
14876
|
+
// so a request a callback makes is the one that lands last.
|
|
14877
|
+
const requestedOpenRef = useRef(false);
|
|
14499
14878
|
const setOpen = useCallback((next) => {
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
});
|
|
14879
|
+
if (requestedOpenRef.current === next)
|
|
14880
|
+
return;
|
|
14881
|
+
requestedOpenRef.current = next;
|
|
14882
|
+
setOpenState(next);
|
|
14883
|
+
if (next) {
|
|
14884
|
+
valueAtOpenRef.current = value;
|
|
14885
|
+
onOpen?.();
|
|
14886
|
+
}
|
|
14887
|
+
else {
|
|
14888
|
+
onClose?.();
|
|
14889
|
+
}
|
|
14512
14890
|
}, [onOpen, onClose, value]);
|
|
14513
14891
|
useEffect(() => {
|
|
14514
14892
|
setText(value ? formatDateTime(value, defaultFormat, locale) : '');
|
|
@@ -14532,18 +14910,18 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14532
14910
|
minutes: value?.getMinutes() ?? 0,
|
|
14533
14911
|
seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
|
|
14534
14912
|
});
|
|
14535
|
-
if (!isWithin(merged,
|
|
14913
|
+
if (!isWithin(merged, lowerBound, max))
|
|
14536
14914
|
return;
|
|
14537
14915
|
commitValue(merged);
|
|
14538
14916
|
setFocusedDate(d);
|
|
14539
|
-
}, [value, enableSeconds,
|
|
14917
|
+
}, [value, enableSeconds, lowerBound, max, commitValue]);
|
|
14540
14918
|
const handleTimeChange = useCallback((parts) => {
|
|
14541
14919
|
const base = value ?? focusedDate;
|
|
14542
14920
|
const next = setTimeOfDay(base, parts);
|
|
14543
|
-
if (!isWithin(next,
|
|
14921
|
+
if (!isWithin(next, lowerBound, max))
|
|
14544
14922
|
return;
|
|
14545
14923
|
commitValue(next);
|
|
14546
|
-
}, [value, focusedDate,
|
|
14924
|
+
}, [value, focusedDate, lowerBound, max, commitValue]);
|
|
14547
14925
|
const tryParse = useCallback((s) => {
|
|
14548
14926
|
const trimmed = s.trim();
|
|
14549
14927
|
if (!trimmed)
|
|
@@ -14578,7 +14956,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14578
14956
|
setText,
|
|
14579
14957
|
makeBaseDate,
|
|
14580
14958
|
locale,
|
|
14581
|
-
min,
|
|
14959
|
+
min: lowerBound,
|
|
14582
14960
|
max,
|
|
14583
14961
|
isBlocked,
|
|
14584
14962
|
disabled,
|
|
@@ -14611,7 +14989,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14611
14989
|
? fromIsoDateTime(e.target.value)
|
|
14612
14990
|
: null;
|
|
14613
14991
|
commitValue(parsed);
|
|
14614
|
-
}, 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 }));
|
|
14615
14993
|
}
|
|
14616
14994
|
const spinnerValue = {
|
|
14617
14995
|
hours: value?.getHours() ?? 0,
|
|
@@ -14638,7 +15016,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14638
15016
|
return (jsxs("div", { ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), ...cleanRest, children: [jsx("input", { ref: combinedRef, type: "text", role: "combobox", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": popoverId, autoComplete: "off", className: inputClass, value: text, placeholder: placeholder, disabled: disabled, readOnly: inputReadOnlyAttr, name: name, form: form, required: required, id: id, ...inputHandlers }), popover && triggerIcon && (jsx("button", { type: "button", className: triggerClass, onClick: () => {
|
|
14639
15017
|
if (canOpen)
|
|
14640
15018
|
setOpen(!open);
|
|
14641
|
-
}, disabled: !canOpen, "aria-label": t.chooseDateTime, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsx(PickerPopover, { isOpen: open, onClose: () => setOpen(false), anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: t.chooseDateTime, id: popoverId, restoreFocusRef: inputRef, children: panel }))] }));
|
|
15019
|
+
}, disabled: !canOpen, "aria-label": t.chooseDateTime, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsx(PickerPopover, { isOpen: open, onClose: () => setOpen(false), anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: t.chooseDateTime, id: popoverId, restoreFocusRef: inputRef, initialFocusSelector: CALENDAR_FOCUSED_CELL, children: panel }))] }));
|
|
14642
15020
|
});
|
|
14643
15021
|
DateTimeInputBase.displayName = 'DateTimeInputBase';
|
|
14644
15022
|
|
|
@@ -14662,26 +15040,34 @@ DateTimeInputBase.displayName = 'DateTimeInputBase';
|
|
|
14662
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) => {
|
|
14663
15041
|
const insideField = useInsideField();
|
|
14664
15042
|
const insideControl = useInsideControl();
|
|
15043
|
+
const ownField = rendersOwnField('DateTimeInput', {
|
|
15044
|
+
insideField,
|
|
15045
|
+
insideControl,
|
|
15046
|
+
label,
|
|
15047
|
+
message,
|
|
15048
|
+
horizontal,
|
|
15049
|
+
fieldClassName,
|
|
15050
|
+
});
|
|
14665
15051
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
14666
15052
|
label,
|
|
14667
15053
|
id: baseProps.id,
|
|
14668
15054
|
labelProps,
|
|
14669
15055
|
// Inline mode renders a bare picker with no input to label.
|
|
14670
|
-
rendersLabel:
|
|
15056
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
14671
15057
|
});
|
|
14672
15058
|
const helpClass = usePrefixedClassNames('help', {
|
|
14673
15059
|
[`is-${messageColor}`]: !!messageColor,
|
|
14674
15060
|
});
|
|
14675
|
-
// The
|
|
14676
|
-
//
|
|
14677
|
-
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 }));
|
|
14678
15064
|
// Inline mode renders a bare picker with no input, so the Control's
|
|
14679
15065
|
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
14680
15066
|
if (!insideControl && !baseProps.inline) {
|
|
14681
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 }));
|
|
14682
15068
|
}
|
|
14683
15069
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
14684
|
-
if (
|
|
15070
|
+
if (ownField) {
|
|
14685
15071
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
14686
15072
|
}
|
|
14687
15073
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -15620,31 +16006,57 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
|
|
|
15620
16006
|
// rewrites the shared element in place rather than removing and recreating
|
|
15621
16007
|
// it between the cleanup and the next run.
|
|
15622
16008
|
useEffect(() => () => setRootThemeRules(rootOrder, ''), [rootOrder]);
|
|
15623
|
-
//
|
|
15624
|
-
//
|
|
15625
|
-
// `
|
|
16009
|
+
// Bulma names its scheme attribute after the class prefix a stylesheet was
|
|
16010
|
+
// built with (`data-<prefix>theme`), so the prefixed builds read
|
|
16011
|
+
// `data-bestax-theme` and never `data-theme`.
|
|
16012
|
+
const classPrefix = useClassPrefix();
|
|
16013
|
+
// Toggle Bulma's light/dark scheme by writing the theme attribute on the
|
|
16014
|
+
// document root (<html>). This is always global, even on a scoped Theme.
|
|
16015
|
+
// Under a class prefix the prefixed attribute is written as well as
|
|
16016
|
+
// `data-theme`, so whichever build is loaded sees it; with no prefix only
|
|
16017
|
+
// `data-theme` is. `'system'` removes them so Bulma follows the OS
|
|
16018
|
+
// preference.
|
|
15626
16019
|
useEffect(() => {
|
|
15627
16020
|
if (colorMode === undefined) {
|
|
15628
16021
|
return;
|
|
15629
16022
|
}
|
|
15630
16023
|
const root = document.documentElement;
|
|
15631
|
-
const
|
|
15632
|
-
|
|
15633
|
-
|
|
15634
|
-
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
|
|
15639
|
-
return () => {
|
|
15640
|
-
if (previous === null) {
|
|
15641
|
-
root.removeAttribute('data-theme');
|
|
16024
|
+
const names = classPrefix
|
|
16025
|
+
? ['data-theme', `data-${classPrefix}theme`]
|
|
16026
|
+
: ['data-theme'];
|
|
16027
|
+
const previous = names.map(name => root.getAttribute(name));
|
|
16028
|
+
for (const name of names) {
|
|
16029
|
+
if (colorMode === 'system') {
|
|
16030
|
+
root.removeAttribute(name);
|
|
16031
|
+
continue;
|
|
15642
16032
|
}
|
|
15643
|
-
|
|
15644
|
-
root.setAttribute(
|
|
16033
|
+
try {
|
|
16034
|
+
root.setAttribute(name, colorMode);
|
|
15645
16035
|
}
|
|
16036
|
+
catch {
|
|
16037
|
+
// A class prefix can hold characters an attribute name cannot.
|
|
16038
|
+
// `data-theme` is still written, as it was before the prefixed
|
|
16039
|
+
// attribute existed, but a sheet built with that prefix keeps its
|
|
16040
|
+
// scheme, so say so.
|
|
16041
|
+
warnOnce('Theme:color-mode-prefix', `[bestax-bulma] <Theme colorMode="${colorMode}">: the class ` +
|
|
16042
|
+
`prefix "${classPrefix}" makes ${name} an invalid attribute ` +
|
|
16043
|
+
'name, so only data-theme is written, and a stylesheet built ' +
|
|
16044
|
+
'with that prefix keeps its scheme.');
|
|
16045
|
+
}
|
|
16046
|
+
}
|
|
16047
|
+
// Restore the previous values when colorMode changes or the component unmounts.
|
|
16048
|
+
return () => {
|
|
16049
|
+
names.forEach((name, i) => {
|
|
16050
|
+
const value = previous[i];
|
|
16051
|
+
if (value === null) {
|
|
16052
|
+
root.removeAttribute(name);
|
|
16053
|
+
}
|
|
16054
|
+
else {
|
|
16055
|
+
root.setAttribute(name, value);
|
|
16056
|
+
}
|
|
16057
|
+
});
|
|
15646
16058
|
};
|
|
15647
|
-
}, [colorMode]);
|
|
16059
|
+
}, [colorMode, classPrefix]);
|
|
15648
16060
|
// For local injection (when isRoot is false), prepare style object for CSS vars
|
|
15649
16061
|
const style = useMemo(() => {
|
|
15650
16062
|
if (isRoot) {
|