@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.cjs
CHANGED
|
@@ -2941,7 +2941,8 @@ function resolvePortalContainer(container) {
|
|
|
2941
2941
|
* Tab order and focus follow the DOM too. Content portaled from inside a
|
|
2942
2942
|
* focus trap is no longer inside the trapped element, so the trap neither
|
|
2943
2943
|
* reaches it nor holds focus there: render nested overlays inside the
|
|
2944
|
-
* trapped element, or give them a trap of their own.
|
|
2944
|
+
* trapped element, or give them a trap of their own. To trap portaled
|
|
2945
|
+
* content, put `useFocusTrap`'s ref on the element inside the `Portal`.
|
|
2945
2946
|
*
|
|
2946
2947
|
* @function
|
|
2947
2948
|
* @param {PortalProps} props - Props for the Portal component.
|
|
@@ -5197,12 +5198,20 @@ const focusedElement = (doc) => getDeepestActiveElement(doc) ?? doc.body;
|
|
|
5197
5198
|
* behaviour, and a modal also has to block the page behind it, which `Modal`
|
|
5198
5199
|
* does. Content the container renders through a portal lives elsewhere in
|
|
5199
5200
|
* the DOM, so it is outside the trap: render nested overlays inside the
|
|
5200
|
-
* container, or give them a trap of their own.
|
|
5201
|
-
*
|
|
5202
|
-
*
|
|
5203
|
-
*
|
|
5204
|
-
*
|
|
5205
|
-
*
|
|
5201
|
+
* container, or give them a trap of their own. To trap content a `Portal`
|
|
5202
|
+
* renders, pass a ref to the element inside the `Portal` as `containerRef`.
|
|
5203
|
+
*
|
|
5204
|
+
* The trap looks at `containerRef` again each time the component calling it
|
|
5205
|
+
* renders. A container that mounts after the trap turns on, such as a panel
|
|
5206
|
+
* waiting for a `Portal` target held in state, has it attach once that
|
|
5207
|
+
* component renders with the container in the DOM. When the container leaves
|
|
5208
|
+
* or is replaced, the trap lets go of it as it would turning off, handing
|
|
5209
|
+
* focus back, and attaches to the next one.
|
|
5210
|
+
* A child that mounts the container in a render of its own (from its own
|
|
5211
|
+
* state, or as a `Suspense` boundary resolves) goes unseen until the
|
|
5212
|
+
* component calling the hook renders again. The trap waits for hydration, so
|
|
5213
|
+
* that component renders again when the page hydrates, and portaled content,
|
|
5214
|
+
* which first appears then, is found.
|
|
5206
5215
|
*
|
|
5207
5216
|
* @function useFocusTrap
|
|
5208
5217
|
* @param containerRef - Ref to the element focus stays inside.
|
|
@@ -5222,12 +5231,18 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5222
5231
|
// Portaled content has no server-rendered counterpart, so a portaled
|
|
5223
5232
|
// container only exists from the commit after hydration.
|
|
5224
5233
|
const hydrated = useIsHydrated();
|
|
5234
|
+
// The container the trap is attached to, or `null` while it has none.
|
|
5235
|
+
const heldRef = React.useRef(null);
|
|
5236
|
+
// Bumped to run the trap again when the container in the ref changes.
|
|
5237
|
+
const [refChanges, setRefChanges] = React.useState(0);
|
|
5225
5238
|
React.useEffect(() => {
|
|
5239
|
+
heldRef.current = null;
|
|
5226
5240
|
if (!active || !hydrated)
|
|
5227
5241
|
return undefined;
|
|
5228
5242
|
const container = containerRef.current;
|
|
5229
5243
|
if (!container)
|
|
5230
5244
|
return undefined;
|
|
5245
|
+
heldRef.current = container;
|
|
5231
5246
|
const doc = container.ownerDocument;
|
|
5232
5247
|
const opener = focusedElement(doc);
|
|
5233
5248
|
// `initialFocusRef` counts only when focusing it puts focus inside. Focus
|
|
@@ -5302,7 +5317,28 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5302
5317
|
target.focus();
|
|
5303
5318
|
}
|
|
5304
5319
|
};
|
|
5305
|
-
}, [
|
|
5320
|
+
}, [
|
|
5321
|
+
active,
|
|
5322
|
+
hydrated,
|
|
5323
|
+
containerRef,
|
|
5324
|
+
initialFocusRef,
|
|
5325
|
+
restoreFocus,
|
|
5326
|
+
refChanges,
|
|
5327
|
+
]);
|
|
5328
|
+
// Nothing says when a ref is set, so after every render, a ref that no
|
|
5329
|
+
// longer matches what the trap holds (a container that mounted, left or was
|
|
5330
|
+
// replaced) runs it again. Effects run in order, so whenever the effect
|
|
5331
|
+
// above ran in this commit, it read the ref just before this does, and a
|
|
5332
|
+
// mismatch means the ref changed in a render it didn't run for. The render
|
|
5333
|
+
// a bump asks for runs it, so the two match and the trap settles. An empty
|
|
5334
|
+
// ref matches a trap holding nothing, so a container that never mounts
|
|
5335
|
+
// costs no render.
|
|
5336
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
|
|
5337
|
+
React.useEffect(() => {
|
|
5338
|
+
if (active && hydrated && containerRef.current !== heldRef.current) {
|
|
5339
|
+
setRefChanges(n => n + 1);
|
|
5340
|
+
}
|
|
5341
|
+
});
|
|
5306
5342
|
}
|
|
5307
5343
|
|
|
5308
5344
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
@@ -5314,6 +5350,15 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5314
5350
|
const useClientLayoutEffect = typeof document === 'undefined' ? React.useEffect : React.useLayoutEffect;
|
|
5315
5351
|
|
|
5316
5352
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
5353
|
+
/**
|
|
5354
|
+
* A `styledGap` for a panel whose stylesheet puts the gap in a margin on the
|
|
5355
|
+
* side facing the anchor, negative on a portaled panel above it, so the
|
|
5356
|
+
* larger magnitude is the gap. In pixels.
|
|
5357
|
+
*/
|
|
5358
|
+
function marginGap(panel) {
|
|
5359
|
+
const { marginTop, marginBottom } = getComputedStyle(panel);
|
|
5360
|
+
return Math.max(Math.abs(parseFloat(marginTop)) || 0, Math.abs(parseFloat(marginBottom)) || 0);
|
|
5361
|
+
}
|
|
5317
5362
|
/**
|
|
5318
5363
|
* The corner that keeps a panel of this size in the viewport, preferring
|
|
5319
5364
|
* below the anchor and lined up with its left edge.
|
|
@@ -5538,15 +5583,6 @@ function nearestDialog(event) {
|
|
|
5538
5583
|
return role === 'dialog' || role === 'alertdialog';
|
|
5539
5584
|
}) ?? null);
|
|
5540
5585
|
}
|
|
5541
|
-
/**
|
|
5542
|
-
* The gap the stylesheet puts between the panel and the trigger, in pixels.
|
|
5543
|
-
* It is a margin on whichever side faces the trigger, and a negative one on
|
|
5544
|
-
* a portaled panel above it, so the larger magnitude is the gap.
|
|
5545
|
-
*/
|
|
5546
|
-
function styledGap(panel) {
|
|
5547
|
-
const { marginTop, marginBottom } = getComputedStyle(panel);
|
|
5548
|
-
return Math.max(Math.abs(parseFloat(marginTop)) || 0, Math.abs(parseFloat(marginBottom)) || 0);
|
|
5549
|
-
}
|
|
5550
5586
|
/**
|
|
5551
5587
|
* 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.
|
|
5552
5588
|
*
|
|
@@ -5598,7 +5634,7 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
|
|
|
5598
5634
|
// The gap comes from `--bulma-popover-offset` in the stylesheet, so the
|
|
5599
5635
|
// coordinates leave it out and `auto` reads it off the panel.
|
|
5600
5636
|
offset: 0,
|
|
5601
|
-
styledGap,
|
|
5637
|
+
styledGap: marginGap,
|
|
5602
5638
|
});
|
|
5603
5639
|
// The trigger renders first inside the wrapper. Read when needed rather
|
|
5604
5640
|
// than held, so a trigger that remounted is still the one found.
|
|
@@ -8735,6 +8771,56 @@ const useInsideControl = () => React.useContext(ControlContext);
|
|
|
8735
8771
|
const FieldProvider = FieldContext.Provider;
|
|
8736
8772
|
/** Provider for Control context — used internally by Control component. */
|
|
8737
8773
|
const ControlProvider = ControlContext.Provider;
|
|
8774
|
+
// Each Field-shaping prop, with what takes its place on a `Field` the caller
|
|
8775
|
+
// wraps around the `Control`. `message` needs nothing there, because the
|
|
8776
|
+
// wrapper still renders it inside `Field > Control`. The other Field-level
|
|
8777
|
+
// props (`labelSize`, `labelProps`, `messageColor`) change nothing unless a
|
|
8778
|
+
// `label` or `message` renders, so on their own they keep no `Field`.
|
|
8779
|
+
// `bare-control.test.tsx` holds this list to what each wrapper hands its
|
|
8780
|
+
// `Field` and to which props change its markup, so a new Field-level prop
|
|
8781
|
+
// fails there until it is sorted into one group or the other.
|
|
8782
|
+
const FIELD_SHAPING = [
|
|
8783
|
+
['label', 'label'],
|
|
8784
|
+
['message', undefined],
|
|
8785
|
+
['horizontal', 'horizontal'],
|
|
8786
|
+
['fieldClassName', 'className (for fieldClassName)'],
|
|
8787
|
+
];
|
|
8788
|
+
/** `a`, `a and b`, `a, b and c`. */
|
|
8789
|
+
const listOf = (items) => items.length > 1
|
|
8790
|
+
? `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`
|
|
8791
|
+
: items[0];
|
|
8792
|
+
/**
|
|
8793
|
+
* Whether a convenience form wrapper renders a `Field` of its own (#905).
|
|
8794
|
+
* Never inside an outer `Field`. Inside a `Control` with no `Field` around
|
|
8795
|
+
* it, a `Field` of its own would put a `.field` inside the `.control`, where
|
|
8796
|
+
* Bulma's control styles break (its icon rules are sibling selectors on the
|
|
8797
|
+
* input). So it renders one there only when a prop that shapes the `Field`'s
|
|
8798
|
+
* markup is set (`label`, `message`, `horizontal`, `fieldClassName`), keeping
|
|
8799
|
+
* the markup it always had, and warns in development that the fix is a
|
|
8800
|
+
* `Field` around that `Control`. A prop counts when it is truthy, the same
|
|
8801
|
+
* test that decides whether it changes the `Field`'s output.
|
|
8802
|
+
* Internal; not part of the public API.
|
|
8803
|
+
*/
|
|
8804
|
+
const rendersOwnField = (component, options) => {
|
|
8805
|
+
if (options.insideField)
|
|
8806
|
+
return false;
|
|
8807
|
+
if (!options.insideControl)
|
|
8808
|
+
return true;
|
|
8809
|
+
const held = FIELD_SHAPING.filter(([prop]) => options[prop]);
|
|
8810
|
+
if (held.length === 0)
|
|
8811
|
+
return false;
|
|
8812
|
+
const names = held.map(([prop]) => prop);
|
|
8813
|
+
const moved = held
|
|
8814
|
+
.map(([, onField]) => onField)
|
|
8815
|
+
.filter((onField) => onField !== undefined);
|
|
8816
|
+
warnOnce(`${component}:Field-in-bare-Control:${names.join('+')}`, `[bestax-bulma] <${component} ${names.join(' ')}> inside a <Control> ` +
|
|
8817
|
+
`with no <Field> around it renders its own <Field> for ` +
|
|
8818
|
+
`${names.length > 1 ? 'those props' : 'that prop'}, which puts a ` +
|
|
8819
|
+
`.field inside the .control, a nesting Bulma's styles are not ` +
|
|
8820
|
+
`written for. Wrap the <Control> in a <Field> instead` +
|
|
8821
|
+
(moved.length > 0 ? `, and set ${listOf(moved)} on that <Field>.` : '.'));
|
|
8822
|
+
return true;
|
|
8823
|
+
};
|
|
8738
8824
|
const FieldLabelIdContext = React.createContext(undefined);
|
|
8739
8825
|
/**
|
|
8740
8826
|
* The id a labeled Field wants its single composed control to adopt (#495).
|
|
@@ -8865,6 +8951,18 @@ const Checkbox = React.forwardRef(({ color, size, className, children, textColor
|
|
|
8865
8951
|
});
|
|
8866
8952
|
Checkbox.displayName = 'Checkbox';
|
|
8867
8953
|
|
|
8954
|
+
// Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
|
|
8955
|
+
// picker bases read it so their right-side launcher gives way to that
|
|
8956
|
+
// spinner, which sits at the same edge, whether the `Control` is the one a
|
|
8957
|
+
// convenience input renders or one the caller wrapped around the input.
|
|
8958
|
+
// Kept apart from `FormContext`, whose contexts only report whether a `Field`
|
|
8959
|
+
// or `Control` is present. Internal; not part of the public API.
|
|
8960
|
+
const ControlLoadingContext = React.createContext(false);
|
|
8961
|
+
/** Provider for the loading state, used internally by `Control`. */
|
|
8962
|
+
const ControlLoadingProvider = ControlLoadingContext.Provider;
|
|
8963
|
+
/** Whether the nearest enclosing `Control` is loading; `false` outside one. */
|
|
8964
|
+
const useControlLoading = () => React.useContext(ControlLoadingContext);
|
|
8965
|
+
|
|
8868
8966
|
const allowedColors = [...validColors, 'inherit', 'current'];
|
|
8869
8967
|
/**
|
|
8870
8968
|
* The `Control` component is a Bulma-styled wrapper for form controls (`Input`, `Select`, `TextArea`, etc.), supporting icons (left/right), loading state, expansion, size, and Bulma helper props for layout and color.
|
|
@@ -8924,7 +9022,7 @@ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isL
|
|
|
8924
9022
|
});
|
|
8925
9023
|
const controlClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
8926
9024
|
// --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
|
|
8927
|
-
return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [children, leftIconValue &&
|
|
9025
|
+
return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsxRuntime.jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
|
|
8928
9026
|
(isIconProps(leftIconValue) ? (jsxRuntime.jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
|
|
8929
9027
|
(isIconProps(rightIconValue) ? (jsxRuntime.jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
|
|
8930
9028
|
});
|
|
@@ -9149,10 +9247,18 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, }) {
|
|
|
9149
9247
|
const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
|
|
9150
9248
|
const insideField = useInsideField();
|
|
9151
9249
|
const insideControl = useInsideControl();
|
|
9250
|
+
const ownField = rendersOwnField('Checkboxes', {
|
|
9251
|
+
insideField,
|
|
9252
|
+
insideControl,
|
|
9253
|
+
label,
|
|
9254
|
+
message,
|
|
9255
|
+
horizontal,
|
|
9256
|
+
fieldClassName,
|
|
9257
|
+
});
|
|
9152
9258
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
9153
9259
|
label,
|
|
9154
9260
|
labelProps,
|
|
9155
|
-
rendersLabel:
|
|
9261
|
+
rendersLabel: ownField,
|
|
9156
9262
|
});
|
|
9157
9263
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
9158
9264
|
...props,
|
|
@@ -9181,7 +9287,7 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
|
|
|
9181
9287
|
if (!insideControl) {
|
|
9182
9288
|
content = jsxRuntime.jsx(Control, { children: content });
|
|
9183
9289
|
}
|
|
9184
|
-
if (
|
|
9290
|
+
if (ownField) {
|
|
9185
9291
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9186
9292
|
}
|
|
9187
9293
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
@@ -9200,11 +9306,20 @@ const File = React.forwardRef(({
|
|
|
9200
9306
|
// Field props
|
|
9201
9307
|
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
|
|
9202
9308
|
const insideField = useInsideField();
|
|
9309
|
+
const insideControl = useInsideControl();
|
|
9310
|
+
const ownField = rendersOwnField('File', {
|
|
9311
|
+
insideField,
|
|
9312
|
+
insideControl,
|
|
9313
|
+
label,
|
|
9314
|
+
message,
|
|
9315
|
+
horizontal,
|
|
9316
|
+
fieldClassName,
|
|
9317
|
+
});
|
|
9203
9318
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
9204
9319
|
label,
|
|
9205
9320
|
id: props.id,
|
|
9206
9321
|
labelProps,
|
|
9207
|
-
rendersLabel:
|
|
9322
|
+
rendersLabel: ownField,
|
|
9208
9323
|
});
|
|
9209
9324
|
const { classPrefix } = useConfig();
|
|
9210
9325
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
@@ -9236,7 +9351,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
9236
9351
|
});
|
|
9237
9352
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
9238
9353
|
const fileElement = (jsxRuntime.jsx("div", { className: fileClass, children: jsxRuntime.jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsxRuntime.jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, ...rest }), jsxRuntime.jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsxRuntime.jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && fileName && (jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: fileName }))] }) }));
|
|
9239
|
-
if (
|
|
9354
|
+
if (ownField) {
|
|
9240
9355
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
|
|
9241
9356
|
}
|
|
9242
9357
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [fileElement, messageEl] }));
|
|
@@ -9369,10 +9484,18 @@ Radio.displayName = 'Radio';
|
|
|
9369
9484
|
const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
|
|
9370
9485
|
const insideField = useInsideField();
|
|
9371
9486
|
const insideControl = useInsideControl();
|
|
9487
|
+
const ownField = rendersOwnField('Radios', {
|
|
9488
|
+
insideField,
|
|
9489
|
+
insideControl,
|
|
9490
|
+
label,
|
|
9491
|
+
message,
|
|
9492
|
+
horizontal,
|
|
9493
|
+
fieldClassName,
|
|
9494
|
+
});
|
|
9372
9495
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
9373
9496
|
label,
|
|
9374
9497
|
labelProps,
|
|
9375
|
-
rendersLabel:
|
|
9498
|
+
rendersLabel: ownField,
|
|
9376
9499
|
});
|
|
9377
9500
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
9378
9501
|
...props,
|
|
@@ -9404,7 +9527,7 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
|
|
|
9404
9527
|
if (!insideControl) {
|
|
9405
9528
|
content = jsxRuntime.jsx(Control, { children: content });
|
|
9406
9529
|
}
|
|
9407
|
-
if (
|
|
9530
|
+
if (ownField) {
|
|
9408
9531
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9409
9532
|
}
|
|
9410
9533
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
@@ -9584,6 +9707,14 @@ const Slider = React.forwardRef((props, ref) => {
|
|
|
9584
9707
|
range: _range, value: _value, defaultValue: _defaultValue, onChange: _onChange, ariaLabel: _ariaLabel, minDistance: _minDistance, nameLow: _nameLow, nameHigh: _nameHigh, ...restProps } = props;
|
|
9585
9708
|
const insideField = useInsideField();
|
|
9586
9709
|
const insideControl = useInsideControl();
|
|
9710
|
+
const ownField = rendersOwnField('Slider', {
|
|
9711
|
+
insideField,
|
|
9712
|
+
insideControl,
|
|
9713
|
+
label,
|
|
9714
|
+
message,
|
|
9715
|
+
horizontal,
|
|
9716
|
+
fieldClassName,
|
|
9717
|
+
});
|
|
9587
9718
|
const { bulmaHelperClasses, rest } = useBulmaClasses(restProps);
|
|
9588
9719
|
// In range mode the low thumb carries `rest` (and any user id), so the
|
|
9589
9720
|
// label targets it; each thumb keeps its own aria-label for AT.
|
|
@@ -9591,7 +9722,7 @@ const Slider = React.forwardRef((props, ref) => {
|
|
|
9591
9722
|
label,
|
|
9592
9723
|
id: restProps.id,
|
|
9593
9724
|
labelProps,
|
|
9594
|
-
rendersLabel:
|
|
9725
|
+
rendersLabel: ownField,
|
|
9595
9726
|
});
|
|
9596
9727
|
// Resolve tooltip mode: explicit tooltip prop takes precedence, else showOutput maps to 'auto'
|
|
9597
9728
|
const tooltipMode = tooltip ?? (showOutput ? 'auto' : 'hidden');
|
|
@@ -9930,7 +10061,7 @@ const Slider = React.forwardRef((props, ref) => {
|
|
|
9930
10061
|
if (!insideControl) {
|
|
9931
10062
|
content = jsxRuntime.jsx(Control, { children: content });
|
|
9932
10063
|
}
|
|
9933
|
-
if (
|
|
10064
|
+
if (ownField) {
|
|
9934
10065
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9935
10066
|
}
|
|
9936
10067
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
@@ -10160,10 +10291,18 @@ function getFillPercent(iconIndex, value) {
|
|
|
10160
10291
|
const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = 0, max = 5, size, disabled = false, showScore = false, showText = false, texts, onChange, customIcon, spaced = false, rtl = false, iconName, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, precision = 1, customText, name, form, className, ...props }, ref) => {
|
|
10161
10292
|
const insideField = useInsideField();
|
|
10162
10293
|
const insideControl = useInsideControl();
|
|
10294
|
+
const ownField = rendersOwnField('Rate', {
|
|
10295
|
+
insideField,
|
|
10296
|
+
insideControl,
|
|
10297
|
+
label,
|
|
10298
|
+
message,
|
|
10299
|
+
horizontal,
|
|
10300
|
+
fieldClassName,
|
|
10301
|
+
});
|
|
10163
10302
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
10164
10303
|
label,
|
|
10165
10304
|
labelProps,
|
|
10166
|
-
rendersLabel:
|
|
10305
|
+
rendersLabel: ownField,
|
|
10167
10306
|
});
|
|
10168
10307
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
10169
10308
|
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
@@ -10366,7 +10505,7 @@ const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, messa
|
|
|
10366
10505
|
if (!insideControl) {
|
|
10367
10506
|
content = jsxRuntime.jsx(Control, { children: content });
|
|
10368
10507
|
}
|
|
10369
|
-
if (
|
|
10508
|
+
if (ownField) {
|
|
10370
10509
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
10371
10510
|
}
|
|
10372
10511
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
@@ -10647,6 +10786,15 @@ const Taginput = React.forwardRef(({
|
|
|
10647
10786
|
// Field props
|
|
10648
10787
|
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = [], data = [], placeholder, field = 'label', allowNew = true, allowDuplicates = false, openOnFocus = false, removeOnKeys = true, confirmKeys = ['Enter', ','], closable = true, attached = false, maxTags, maxlength, disabled = false, readonly = false, rounded = false, ellipsis = false, hasCounter = true, onPasteSeparators = [','], beforeAdding, createTag, keepFirst = false, keepOpen = true, loading = false, ariaCloseLabel, icon, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, tagColor, size, onChange, onAdd, onRemove, onTyping, tagTemplate, name, form, id, className, ...props }, ref) => {
|
|
10649
10788
|
const insideField = useInsideField();
|
|
10789
|
+
const insideControl = useInsideControl();
|
|
10790
|
+
const ownField = rendersOwnField('Taginput', {
|
|
10791
|
+
insideField,
|
|
10792
|
+
insideControl,
|
|
10793
|
+
label,
|
|
10794
|
+
message,
|
|
10795
|
+
horizontal,
|
|
10796
|
+
fieldClassName,
|
|
10797
|
+
});
|
|
10650
10798
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
10651
10799
|
const { classPrefix } = useConfig();
|
|
10652
10800
|
const defaultIconLibrary = useIconLibrary();
|
|
@@ -10668,7 +10816,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
10668
10816
|
label,
|
|
10669
10817
|
id,
|
|
10670
10818
|
labelProps,
|
|
10671
|
-
rendersLabel:
|
|
10819
|
+
rendersLabel: ownField && !isMaxReached,
|
|
10672
10820
|
});
|
|
10673
10821
|
// Get display value from tag
|
|
10674
10822
|
const getDisplayValue = (tag) => {
|
|
@@ -10940,7 +11088,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
10940
11088
|
'is-active': index === highlightedIndex,
|
|
10941
11089
|
}), onClick: () => addTag(item), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": index === highlightedIndex, children: item }, index))) }) })), showCounter && jsxRuntime.jsx("small", { className: counterClasses, children: counterText }), name &&
|
|
10942
11090
|
tags.map((tag, i) => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
|
|
10943
|
-
if (
|
|
11091
|
+
if (ownField) {
|
|
10944
11092
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
|
|
10945
11093
|
}
|
|
10946
11094
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [taginputElement, messageEl] }));
|
|
@@ -11062,7 +11210,7 @@ const TextAreaBase = React.forwardRef(({ color, size, isRounded, isStatic, isHov
|
|
|
11062
11210
|
TextAreaBase.displayName = 'TextAreaBase';
|
|
11063
11211
|
|
|
11064
11212
|
/**
|
|
11065
|
-
* The `Input` component provides a Bulma-styled text input, supporting colors, sizes, rounded corners, static
|
|
11213
|
+
* 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.
|
|
11066
11214
|
*
|
|
11067
11215
|
* @function
|
|
11068
11216
|
* @param {InputProps} props - Props for Input.
|
|
@@ -11093,11 +11241,19 @@ fieldClassName, controlClassName,
|
|
|
11093
11241
|
...inputProps }, ref) => {
|
|
11094
11242
|
const insideField = useInsideField();
|
|
11095
11243
|
const insideControl = useInsideControl();
|
|
11244
|
+
const ownField = rendersOwnField('Input', {
|
|
11245
|
+
insideField,
|
|
11246
|
+
insideControl,
|
|
11247
|
+
label,
|
|
11248
|
+
message,
|
|
11249
|
+
horizontal,
|
|
11250
|
+
fieldClassName,
|
|
11251
|
+
});
|
|
11096
11252
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11097
11253
|
label,
|
|
11098
11254
|
id: inputProps.id,
|
|
11099
11255
|
labelProps,
|
|
11100
|
-
rendersLabel:
|
|
11256
|
+
rendersLabel: ownField,
|
|
11101
11257
|
});
|
|
11102
11258
|
const helpClass = usePrefixedClassNames('help', {
|
|
11103
11259
|
[`is-${messageColor}`]: !!messageColor,
|
|
@@ -11107,7 +11263,7 @@ fieldClassName, controlClassName,
|
|
|
11107
11263
|
content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
11108
11264
|
}
|
|
11109
11265
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
11110
|
-
if (
|
|
11266
|
+
if (ownField) {
|
|
11111
11267
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11112
11268
|
}
|
|
11113
11269
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
@@ -11143,11 +11299,19 @@ fieldClassName, controlClassName,
|
|
|
11143
11299
|
...selectProps }, ref) => {
|
|
11144
11300
|
const insideField = useInsideField();
|
|
11145
11301
|
const insideControl = useInsideControl();
|
|
11302
|
+
const ownField = rendersOwnField('Select', {
|
|
11303
|
+
insideField,
|
|
11304
|
+
insideControl,
|
|
11305
|
+
label,
|
|
11306
|
+
message,
|
|
11307
|
+
horizontal,
|
|
11308
|
+
fieldClassName,
|
|
11309
|
+
});
|
|
11146
11310
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11147
11311
|
label,
|
|
11148
11312
|
id: selectProps.id,
|
|
11149
11313
|
labelProps,
|
|
11150
|
-
rendersLabel:
|
|
11314
|
+
rendersLabel: ownField,
|
|
11151
11315
|
});
|
|
11152
11316
|
const helpClass = usePrefixedClassNames('help', {
|
|
11153
11317
|
[`is-${messageColor}`]: !!messageColor,
|
|
@@ -11157,7 +11321,7 @@ fieldClassName, controlClassName,
|
|
|
11157
11321
|
content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
11158
11322
|
}
|
|
11159
11323
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
11160
|
-
if (
|
|
11324
|
+
if (ownField) {
|
|
11161
11325
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11162
11326
|
}
|
|
11163
11327
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
@@ -11165,7 +11329,7 @@ fieldClassName, controlClassName,
|
|
|
11165
11329
|
Select.displayName = 'Select';
|
|
11166
11330
|
|
|
11167
11331
|
/**
|
|
11168
|
-
* The `TextArea` component provides a Bulma-styled multi-line text input, supporting color, size,
|
|
11332
|
+
* 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.
|
|
11169
11333
|
*
|
|
11170
11334
|
* @function
|
|
11171
11335
|
* @param {TextAreaProps} props - Props for TextArea.
|
|
@@ -11194,11 +11358,19 @@ fieldClassName, controlClassName,
|
|
|
11194
11358
|
...textAreaProps }, ref) => {
|
|
11195
11359
|
const insideField = useInsideField();
|
|
11196
11360
|
const insideControl = useInsideControl();
|
|
11361
|
+
const ownField = rendersOwnField('TextArea', {
|
|
11362
|
+
insideField,
|
|
11363
|
+
insideControl,
|
|
11364
|
+
label,
|
|
11365
|
+
message,
|
|
11366
|
+
horizontal,
|
|
11367
|
+
fieldClassName,
|
|
11368
|
+
});
|
|
11197
11369
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11198
11370
|
label,
|
|
11199
11371
|
id: textAreaProps.id,
|
|
11200
11372
|
labelProps,
|
|
11201
|
-
rendersLabel:
|
|
11373
|
+
rendersLabel: ownField,
|
|
11202
11374
|
});
|
|
11203
11375
|
const helpClass = usePrefixedClassNames('help', {
|
|
11204
11376
|
[`is-${messageColor}`]: !!messageColor,
|
|
@@ -11208,7 +11380,7 @@ fieldClassName, controlClassName,
|
|
|
11208
11380
|
content = (jsxRuntime.jsx(Control, { isLoading: controlIsLoading, size: controlSize, className: controlClassName, children: content }));
|
|
11209
11381
|
}
|
|
11210
11382
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
11211
|
-
if (
|
|
11383
|
+
if (ownField) {
|
|
11212
11384
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11213
11385
|
}
|
|
11214
11386
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
@@ -11251,355 +11423,90 @@ const mergeLabels = (overrides) => ({
|
|
|
11251
11423
|
...(overrides ?? {}),
|
|
11252
11424
|
});
|
|
11253
11425
|
|
|
11254
|
-
|
|
11255
|
-
|
|
11256
|
-
|
|
11257
|
-
|
|
11258
|
-
const DEFAULT_TIME_FORMAT_12 = 'hh:mm A';
|
|
11259
|
-
const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm';
|
|
11260
|
-
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
11261
|
-
const TOKEN_RE = /YYYY|YY|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|A|a/g;
|
|
11262
|
-
function formatToken(d, token) {
|
|
11263
|
-
switch (token) {
|
|
11264
|
-
case 'YYYY':
|
|
11265
|
-
// Zero-pad to a fixed 4 chars so the rendered width matches the
|
|
11266
|
-
// segment-map width during digit-by-digit keyboard entry (and to match
|
|
11267
|
-
// ISO 8601). Years >9999 keep their natural width.
|
|
11268
|
-
return pad(d.getFullYear(), 4);
|
|
11269
|
-
case 'YY':
|
|
11270
|
-
return pad(d.getFullYear() % 100);
|
|
11271
|
-
case 'MM':
|
|
11272
|
-
return pad(d.getMonth() + 1);
|
|
11273
|
-
case 'M':
|
|
11274
|
-
return String(d.getMonth() + 1);
|
|
11275
|
-
case 'DD':
|
|
11276
|
-
return pad(d.getDate());
|
|
11277
|
-
case 'D':
|
|
11278
|
-
return String(d.getDate());
|
|
11279
|
-
case 'HH':
|
|
11280
|
-
return pad(d.getHours());
|
|
11281
|
-
case 'H':
|
|
11282
|
-
return String(d.getHours());
|
|
11283
|
-
case 'hh': {
|
|
11284
|
-
const h12 = d.getHours() % 12 || 12;
|
|
11285
|
-
return pad(h12);
|
|
11286
|
-
}
|
|
11287
|
-
case 'h': {
|
|
11288
|
-
const h12 = d.getHours() % 12 || 12;
|
|
11289
|
-
return String(h12);
|
|
11290
|
-
}
|
|
11291
|
-
case 'mm':
|
|
11292
|
-
return pad(d.getMinutes());
|
|
11293
|
-
case 'm':
|
|
11294
|
-
return String(d.getMinutes());
|
|
11295
|
-
case 'ss':
|
|
11296
|
-
return pad(d.getSeconds());
|
|
11297
|
-
case 's':
|
|
11298
|
-
return String(d.getSeconds());
|
|
11299
|
-
case 'A':
|
|
11300
|
-
return d.getHours() < 12 ? 'AM' : 'PM';
|
|
11301
|
-
case 'a':
|
|
11302
|
-
return d.getHours() < 12 ? 'am' : 'pm';
|
|
11303
|
-
default:
|
|
11304
|
-
return token;
|
|
11305
|
-
}
|
|
11426
|
+
function isSameDay(a, b) {
|
|
11427
|
+
return (a.getFullYear() === b.getFullYear() &&
|
|
11428
|
+
a.getMonth() === b.getMonth() &&
|
|
11429
|
+
a.getDate() === b.getDate());
|
|
11306
11430
|
}
|
|
11307
|
-
function
|
|
11308
|
-
return
|
|
11431
|
+
function isSameMonth(a, b) {
|
|
11432
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
|
|
11309
11433
|
}
|
|
11310
|
-
function
|
|
11311
|
-
return
|
|
11434
|
+
function isBefore(a, b) {
|
|
11435
|
+
return a.getTime() < b.getTime();
|
|
11312
11436
|
}
|
|
11313
|
-
function
|
|
11314
|
-
|
|
11315
|
-
if (typeof f === 'string')
|
|
11316
|
-
return formatTokenString(d, f);
|
|
11317
|
-
return formatWithIntl(d, f, locale);
|
|
11437
|
+
function isAfter(a, b) {
|
|
11438
|
+
return a.getTime() > b.getTime();
|
|
11318
11439
|
}
|
|
11319
|
-
function
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11440
|
+
function isWithin(d, min, max) {
|
|
11441
|
+
if (min && isBefore(d, min))
|
|
11442
|
+
return false;
|
|
11443
|
+
if (max && isAfter(d, max))
|
|
11444
|
+
return false;
|
|
11445
|
+
return true;
|
|
11324
11446
|
}
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11447
|
+
// The boundaries build on `makeDate` (below), so a date in years 0–99 keeps
|
|
11448
|
+
// its year.
|
|
11449
|
+
function startOfDay(d) {
|
|
11450
|
+
return makeDate(d.getFullYear(), d.getMonth(), d.getDate());
|
|
11451
|
+
}
|
|
11452
|
+
function endOfDay(d) {
|
|
11453
|
+
const r = makeDate(d.getFullYear(), d.getMonth(), d.getDate());
|
|
11454
|
+
r.setHours(23, 59, 59, 999);
|
|
11455
|
+
return r;
|
|
11456
|
+
}
|
|
11457
|
+
function startOfMonth(d) {
|
|
11458
|
+
return makeDate(d.getFullYear(), d.getMonth());
|
|
11330
11459
|
}
|
|
11331
11460
|
/**
|
|
11332
|
-
*
|
|
11333
|
-
*
|
|
11334
|
-
*
|
|
11335
|
-
* Returns `null` when the cycle can't be read from the format — it's an
|
|
11336
|
-
* `Intl.DateTimeFormat` options object (no token string), `undefined`, or has
|
|
11337
|
-
* no hour token — so callers fall back to the raw `hourFormat` prop. Scans with
|
|
11338
|
-
* the same {@link TOKEN_RE} grammar the input renders with, so the derived cycle
|
|
11339
|
-
* always agrees with what the field actually displays.
|
|
11461
|
+
* Local midnight on `year`-`month`-`day` (`month` from 0), like the
|
|
11462
|
+
* `Date(y, m, d)` constructor except that years 0–99 stay as given rather than
|
|
11463
|
+
* turning into 1900–1999.
|
|
11340
11464
|
*/
|
|
11341
|
-
function
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
if (token === 'H' || token === 'HH')
|
|
11346
|
-
return '24';
|
|
11347
|
-
if (token === 'h' || token === 'hh')
|
|
11348
|
-
return '12';
|
|
11349
|
-
}
|
|
11350
|
-
return null;
|
|
11465
|
+
function makeDate(year, month = 0, day = 1) {
|
|
11466
|
+
const d = new Date(2000, 0, 1);
|
|
11467
|
+
d.setFullYear(year, month, day);
|
|
11468
|
+
return d;
|
|
11351
11469
|
}
|
|
11352
11470
|
/**
|
|
11353
|
-
*
|
|
11354
|
-
*
|
|
11355
|
-
*
|
|
11471
|
+
* The earliest year the pickers accept. HTML's date, month and datetime-local
|
|
11472
|
+
* inputs hold no year below 1, so the calendar, typing and parsing stop there
|
|
11473
|
+
* too.
|
|
11356
11474
|
*/
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
const year = parts.year ?? new Date().getFullYear();
|
|
11367
|
-
const month = (parts.month ?? 1) - 1;
|
|
11368
|
-
const day = parts.day ?? 1;
|
|
11369
|
-
const hours = parts.hours ?? 0;
|
|
11370
|
-
const minutes = parts.minutes ?? 0;
|
|
11371
|
-
const seconds = parts.seconds ?? 0;
|
|
11372
|
-
const d = new Date(year, month, day, hours, minutes, seconds, 0);
|
|
11373
|
-
if (d.getFullYear() !== year ||
|
|
11374
|
-
d.getMonth() !== month ||
|
|
11375
|
-
d.getDate() !== day) {
|
|
11376
|
-
return null;
|
|
11377
|
-
}
|
|
11378
|
-
return d;
|
|
11475
|
+
const FIRST_YEAR = 1;
|
|
11476
|
+
/**
|
|
11477
|
+
* The lower bound a picker applies: `min`, raised to local midnight on
|
|
11478
|
+
* 1 January of {@link FIRST_YEAR} when it is earlier or absent. A `min` in
|
|
11479
|
+
* range comes back as the same object.
|
|
11480
|
+
*/
|
|
11481
|
+
function floorMin(min) {
|
|
11482
|
+
const floor = makeDate(FIRST_YEAR);
|
|
11483
|
+
return min && min.getTime() >= floor.getTime() ? min : floor;
|
|
11379
11484
|
}
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11385
|
-
|
|
11386
|
-
|
|
11387
|
-
const
|
|
11388
|
-
|
|
11485
|
+
/**
|
|
11486
|
+
* First instant of the day, month or year containing `d`. Works by setters on
|
|
11487
|
+
* a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
|
|
11488
|
+
* as 1900–1999: segmented typing passes through such years on the way to a
|
|
11489
|
+
* four-digit one.
|
|
11490
|
+
*/
|
|
11491
|
+
function startOfPeriod(d, granularity) {
|
|
11492
|
+
const r = new Date(d);
|
|
11493
|
+
if (granularity === 'year')
|
|
11494
|
+
r.setMonth(0, 1);
|
|
11495
|
+
else if (granularity === 'month')
|
|
11496
|
+
r.setDate(1);
|
|
11497
|
+
r.setHours(0, 0, 0, 0);
|
|
11498
|
+
return r;
|
|
11389
11499
|
}
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
const one = fmt[i];
|
|
11401
|
-
let token = null;
|
|
11402
|
-
if (four === 'YYYY')
|
|
11403
|
-
token = 'YYYY';
|
|
11404
|
-
else if (two === 'YY' ||
|
|
11405
|
-
two === 'MM' ||
|
|
11406
|
-
two === 'DD' ||
|
|
11407
|
-
two === 'HH' ||
|
|
11408
|
-
two === 'hh' ||
|
|
11409
|
-
two === 'mm' ||
|
|
11410
|
-
two === 'ss')
|
|
11411
|
-
token = two;
|
|
11412
|
-
else if (one === 'M' ||
|
|
11413
|
-
one === 'D' ||
|
|
11414
|
-
one === 'H' ||
|
|
11415
|
-
one === 'h' ||
|
|
11416
|
-
one === 'm' ||
|
|
11417
|
-
one === 's' ||
|
|
11418
|
-
one === 'A' ||
|
|
11419
|
-
one === 'a')
|
|
11420
|
-
token = one;
|
|
11421
|
-
if (token) {
|
|
11422
|
-
tokenOrder.push(token);
|
|
11423
|
-
switch (token) {
|
|
11424
|
-
case 'YYYY':
|
|
11425
|
-
regex += '(\\d{4})';
|
|
11426
|
-
break;
|
|
11427
|
-
case 'YY':
|
|
11428
|
-
regex += '(\\d{2})';
|
|
11429
|
-
break;
|
|
11430
|
-
case 'MM':
|
|
11431
|
-
case 'DD':
|
|
11432
|
-
case 'HH':
|
|
11433
|
-
case 'hh':
|
|
11434
|
-
case 'mm':
|
|
11435
|
-
case 'ss':
|
|
11436
|
-
regex += '(\\d{1,2})';
|
|
11437
|
-
break;
|
|
11438
|
-
case 'M':
|
|
11439
|
-
case 'D':
|
|
11440
|
-
case 'H':
|
|
11441
|
-
case 'h':
|
|
11442
|
-
case 'm':
|
|
11443
|
-
case 's':
|
|
11444
|
-
regex += '(\\d{1,2})';
|
|
11445
|
-
break;
|
|
11446
|
-
case 'A':
|
|
11447
|
-
regex += '(AM|PM)';
|
|
11448
|
-
break;
|
|
11449
|
-
case 'a':
|
|
11450
|
-
regex += '(am|pm)';
|
|
11451
|
-
break;
|
|
11452
|
-
}
|
|
11453
|
-
i += token.length;
|
|
11454
|
-
}
|
|
11455
|
-
else {
|
|
11456
|
-
regex += escapeRegExp(one);
|
|
11457
|
-
i += 1;
|
|
11458
|
-
}
|
|
11459
|
-
}
|
|
11460
|
-
const m = new RegExp('^' + regex + '$').exec(input);
|
|
11461
|
-
if (!m)
|
|
11462
|
-
return null;
|
|
11463
|
-
const out = {};
|
|
11464
|
-
tokenOrder.forEach((token, idx) => {
|
|
11465
|
-
const v = m[idx + 1];
|
|
11466
|
-
switch (token) {
|
|
11467
|
-
case 'YYYY':
|
|
11468
|
-
out.year = Number(v);
|
|
11469
|
-
break;
|
|
11470
|
-
case 'YY':
|
|
11471
|
-
out.year = 2000 + Number(v);
|
|
11472
|
-
break;
|
|
11473
|
-
case 'MM':
|
|
11474
|
-
case 'M':
|
|
11475
|
-
out.month = Number(v);
|
|
11476
|
-
break;
|
|
11477
|
-
case 'DD':
|
|
11478
|
-
case 'D':
|
|
11479
|
-
out.day = Number(v);
|
|
11480
|
-
break;
|
|
11481
|
-
case 'HH':
|
|
11482
|
-
case 'H':
|
|
11483
|
-
out.hours = Number(v);
|
|
11484
|
-
break;
|
|
11485
|
-
case 'hh':
|
|
11486
|
-
case 'h':
|
|
11487
|
-
out.hours = Number(v);
|
|
11488
|
-
break;
|
|
11489
|
-
case 'mm':
|
|
11490
|
-
case 'm':
|
|
11491
|
-
out.minutes = Number(v);
|
|
11492
|
-
break;
|
|
11493
|
-
case 'ss':
|
|
11494
|
-
case 's':
|
|
11495
|
-
out.seconds = Number(v);
|
|
11496
|
-
break;
|
|
11497
|
-
case 'A':
|
|
11498
|
-
out.meridiem = v.toLowerCase() === 'am' ? 'am' : 'pm';
|
|
11499
|
-
break;
|
|
11500
|
-
case 'a':
|
|
11501
|
-
out.meridiem = v === 'am' ? 'am' : 'pm';
|
|
11502
|
-
break;
|
|
11503
|
-
}
|
|
11504
|
-
});
|
|
11505
|
-
if (out.meridiem !== undefined && out.hours !== undefined) {
|
|
11506
|
-
if (out.meridiem === 'pm' && out.hours < 12)
|
|
11507
|
-
out.hours += 12;
|
|
11508
|
-
if (out.meridiem === 'am' && out.hours === 12)
|
|
11509
|
-
out.hours = 0;
|
|
11510
|
-
}
|
|
11511
|
-
return out;
|
|
11512
|
-
}
|
|
11513
|
-
/**
|
|
11514
|
-
* Locale-aware day name labels in calendar order (Sunday → Saturday).
|
|
11515
|
-
* Caller rotates by `firstDayOfWeek`.
|
|
11516
|
-
*/
|
|
11517
|
-
function getDayNames(locale, length = 'short') {
|
|
11518
|
-
const fmt = new Intl.DateTimeFormat(locale, { weekday: length });
|
|
11519
|
-
// 2021-01-03 was a Sunday; use it as the anchor.
|
|
11520
|
-
const sunday = new Date(2021, 0, 3);
|
|
11521
|
-
const out = [];
|
|
11522
|
-
for (let i = 0; i < 7; i++) {
|
|
11523
|
-
const d = new Date(sunday);
|
|
11524
|
-
d.setDate(sunday.getDate() + i);
|
|
11525
|
-
out.push(fmt.format(d));
|
|
11526
|
-
}
|
|
11527
|
-
return out;
|
|
11528
|
-
}
|
|
11529
|
-
function getMonthNames(locale, length = 'long') {
|
|
11530
|
-
const fmt = new Intl.DateTimeFormat(locale, { month: length });
|
|
11531
|
-
const out = [];
|
|
11532
|
-
for (let i = 0; i < 12; i++) {
|
|
11533
|
-
out.push(fmt.format(new Date(2021, i, 1)));
|
|
11534
|
-
}
|
|
11535
|
-
return out;
|
|
11536
|
-
}
|
|
11537
|
-
|
|
11538
|
-
function isSameDay(a, b) {
|
|
11539
|
-
return (a.getFullYear() === b.getFullYear() &&
|
|
11540
|
-
a.getMonth() === b.getMonth() &&
|
|
11541
|
-
a.getDate() === b.getDate());
|
|
11542
|
-
}
|
|
11543
|
-
function isSameMonth(a, b) {
|
|
11544
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
|
|
11545
|
-
}
|
|
11546
|
-
function isBefore(a, b) {
|
|
11547
|
-
return a.getTime() < b.getTime();
|
|
11548
|
-
}
|
|
11549
|
-
function isAfter(a, b) {
|
|
11550
|
-
return a.getTime() > b.getTime();
|
|
11551
|
-
}
|
|
11552
|
-
function isWithin(d, min, max) {
|
|
11553
|
-
if (min && isBefore(d, min))
|
|
11554
|
-
return false;
|
|
11555
|
-
if (max && isAfter(d, max))
|
|
11556
|
-
return false;
|
|
11557
|
-
return true;
|
|
11558
|
-
}
|
|
11559
|
-
function startOfDay(d) {
|
|
11560
|
-
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
|
11561
|
-
}
|
|
11562
|
-
function endOfDay(d) {
|
|
11563
|
-
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
|
|
11564
|
-
}
|
|
11565
|
-
function startOfMonth(d) {
|
|
11566
|
-
return new Date(d.getFullYear(), d.getMonth(), 1, 0, 0, 0, 0);
|
|
11567
|
-
}
|
|
11568
|
-
/**
|
|
11569
|
-
* Local midnight on `year`-`month`-`day` (`month` from 0), like the
|
|
11570
|
-
* `Date(y, m, d)` constructor except that years 0–99 stay as given rather than
|
|
11571
|
-
* turning into 1900–1999.
|
|
11572
|
-
*/
|
|
11573
|
-
function makeDate(year, month = 0, day = 1) {
|
|
11574
|
-
const d = new Date(2000, 0, 1);
|
|
11575
|
-
d.setFullYear(year, month, day);
|
|
11576
|
-
return d;
|
|
11577
|
-
}
|
|
11578
|
-
/**
|
|
11579
|
-
* First instant of the day, month or year containing `d`. Works by setters on
|
|
11580
|
-
* a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
|
|
11581
|
-
* as 1900–1999: segmented typing passes through such years on the way to a
|
|
11582
|
-
* four-digit one.
|
|
11583
|
-
*/
|
|
11584
|
-
function startOfPeriod(d, granularity) {
|
|
11585
|
-
const r = new Date(d);
|
|
11586
|
-
if (granularity === 'year')
|
|
11587
|
-
r.setMonth(0, 1);
|
|
11588
|
-
else if (granularity === 'month')
|
|
11589
|
-
r.setDate(1);
|
|
11590
|
-
r.setHours(0, 0, 0, 0);
|
|
11591
|
-
return r;
|
|
11592
|
-
}
|
|
11593
|
-
/** Last instant of the day, month or year containing `d`. */
|
|
11594
|
-
function endOfPeriod(d, granularity) {
|
|
11595
|
-
const r = new Date(d);
|
|
11596
|
-
if (granularity === 'year')
|
|
11597
|
-
r.setMonth(11, 31);
|
|
11598
|
-
// Day 0 of the next month is this month's last day.
|
|
11599
|
-
else if (granularity === 'month')
|
|
11600
|
-
r.setMonth(r.getMonth() + 1, 0);
|
|
11601
|
-
r.setHours(23, 59, 59, 999);
|
|
11602
|
-
return r;
|
|
11500
|
+
/** Last instant of the day, month or year containing `d`. */
|
|
11501
|
+
function endOfPeriod(d, granularity) {
|
|
11502
|
+
const r = new Date(d);
|
|
11503
|
+
if (granularity === 'year')
|
|
11504
|
+
r.setMonth(11, 31);
|
|
11505
|
+
// Day 0 of the next month is this month's last day.
|
|
11506
|
+
else if (granularity === 'month')
|
|
11507
|
+
r.setMonth(r.getMonth() + 1, 0);
|
|
11508
|
+
r.setHours(23, 59, 59, 999);
|
|
11509
|
+
return r;
|
|
11603
11510
|
}
|
|
11604
11511
|
/**
|
|
11605
11512
|
* Whether the day `d` falls outside `[min, max]` or is blocked by
|
|
@@ -11651,7 +11558,7 @@ function addMonths(d, n) {
|
|
|
11651
11558
|
r.setDate(1);
|
|
11652
11559
|
r.setMonth(r.getMonth() + n);
|
|
11653
11560
|
// Clamp day to last day of new month if original day is out of range.
|
|
11654
|
-
const lastDay =
|
|
11561
|
+
const lastDay = makeDate(r.getFullYear(), r.getMonth() + 1, 0).getDate();
|
|
11655
11562
|
r.setDate(Math.min(day, lastDay));
|
|
11656
11563
|
return r;
|
|
11657
11564
|
}
|
|
@@ -11668,12 +11575,14 @@ function setTimeOfDay(d, parts) {
|
|
|
11668
11575
|
r.setSeconds(parts.seconds);
|
|
11669
11576
|
return r;
|
|
11670
11577
|
}
|
|
11578
|
+
/**
|
|
11579
|
+
* A copy of `d` moved into `[min, max]`. When `min` is after `max` nothing is
|
|
11580
|
+
* in range and `min` wins, so clamping twice gives the same date and a
|
|
11581
|
+
* picker's focus stays off the years before 1 under a `max` before then.
|
|
11582
|
+
*/
|
|
11671
11583
|
function clampDate(d, min, max) {
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
if (max && isAfter(d, max))
|
|
11675
|
-
return new Date(max);
|
|
11676
|
-
return new Date(d);
|
|
11584
|
+
const capped = max && isAfter(d, max) ? max : d;
|
|
11585
|
+
return new Date(min && isBefore(capped, min) ? min : capped);
|
|
11677
11586
|
}
|
|
11678
11587
|
/**
|
|
11679
11588
|
* Snap a Date's time-of-day to the nearest grid defined by the given
|
|
@@ -11695,44 +11604,337 @@ function snapTimeToIncrement(d, opts = {}) {
|
|
|
11695
11604
|
s -= 60;
|
|
11696
11605
|
m += 1;
|
|
11697
11606
|
}
|
|
11698
|
-
}
|
|
11699
|
-
if (incrementMinutes > 1) {
|
|
11700
|
-
m = Math.round(m / incrementMinutes) * incrementMinutes;
|
|
11701
|
-
if (m >= 60) {
|
|
11702
|
-
m -= 60;
|
|
11703
|
-
h += 1;
|
|
11607
|
+
}
|
|
11608
|
+
if (incrementMinutes > 1) {
|
|
11609
|
+
m = Math.round(m / incrementMinutes) * incrementMinutes;
|
|
11610
|
+
if (m >= 60) {
|
|
11611
|
+
m -= 60;
|
|
11612
|
+
h += 1;
|
|
11613
|
+
}
|
|
11614
|
+
}
|
|
11615
|
+
if (incrementHours > 1) {
|
|
11616
|
+
h = Math.round(h / incrementHours) * incrementHours;
|
|
11617
|
+
// setHours handles 24+ overflow naturally (rolls forward into next day).
|
|
11618
|
+
}
|
|
11619
|
+
r.setHours(h, m, s, 0);
|
|
11620
|
+
return r;
|
|
11621
|
+
}
|
|
11622
|
+
/**
|
|
11623
|
+
* Build a 6-week × 7-day grid (42 cells) anchored on the month containing
|
|
11624
|
+
* `monthAnchor`. The first cell is the start of the week containing the first
|
|
11625
|
+
* of the month, where the week starts on `firstDayOfWeek`.
|
|
11626
|
+
*/
|
|
11627
|
+
function buildMonthGrid(monthAnchor, firstDayOfWeek = 0) {
|
|
11628
|
+
const monthStart = startOfMonth(monthAnchor);
|
|
11629
|
+
const firstWeekday = monthStart.getDay();
|
|
11630
|
+
const offset = (firstWeekday - firstDayOfWeek + 7) % 7;
|
|
11631
|
+
const gridStart = addDays(monthStart, -offset);
|
|
11632
|
+
const today = startOfDay(new Date());
|
|
11633
|
+
const cells = [];
|
|
11634
|
+
for (let i = 0; i < 42; i++) {
|
|
11635
|
+
const date = addDays(gridStart, i);
|
|
11636
|
+
cells.push({
|
|
11637
|
+
date,
|
|
11638
|
+
inCurrentMonth: date.getMonth() === monthAnchor.getMonth(),
|
|
11639
|
+
isToday: isSameDay(date, today),
|
|
11640
|
+
});
|
|
11641
|
+
}
|
|
11642
|
+
return cells;
|
|
11643
|
+
}
|
|
11644
|
+
|
|
11645
|
+
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
|
|
11646
|
+
const DEFAULT_MONTH_FORMAT = 'YYYY-MM';
|
|
11647
|
+
const DEFAULT_YEAR_FORMAT = 'YYYY';
|
|
11648
|
+
const DEFAULT_TIME_FORMAT_24 = 'HH:mm';
|
|
11649
|
+
const DEFAULT_TIME_FORMAT_12 = 'hh:mm A';
|
|
11650
|
+
const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DD HH:mm';
|
|
11651
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
11652
|
+
const TOKEN_RE = /YYYY|YY|MM|M|DD|D|HH|H|hh|h|mm|m|ss|s|A|a/g;
|
|
11653
|
+
function formatToken(d, token) {
|
|
11654
|
+
switch (token) {
|
|
11655
|
+
case 'YYYY':
|
|
11656
|
+
// Zero-pad to a fixed 4 chars so the rendered width matches the
|
|
11657
|
+
// segment-map width during digit-by-digit keyboard entry (and to match
|
|
11658
|
+
// ISO 8601), so year 19 shows as `0019`. Years >9999 keep their natural
|
|
11659
|
+
// width. The pickers commit no year below 1, so a minus sign reaches
|
|
11660
|
+
// this only from a `value` passed in.
|
|
11661
|
+
return pad(d.getFullYear(), 4);
|
|
11662
|
+
case 'YY':
|
|
11663
|
+
return pad(d.getFullYear() % 100);
|
|
11664
|
+
case 'MM':
|
|
11665
|
+
return pad(d.getMonth() + 1);
|
|
11666
|
+
case 'M':
|
|
11667
|
+
return String(d.getMonth() + 1);
|
|
11668
|
+
case 'DD':
|
|
11669
|
+
return pad(d.getDate());
|
|
11670
|
+
case 'D':
|
|
11671
|
+
return String(d.getDate());
|
|
11672
|
+
case 'HH':
|
|
11673
|
+
return pad(d.getHours());
|
|
11674
|
+
case 'H':
|
|
11675
|
+
return String(d.getHours());
|
|
11676
|
+
case 'hh': {
|
|
11677
|
+
const h12 = d.getHours() % 12 || 12;
|
|
11678
|
+
return pad(h12);
|
|
11679
|
+
}
|
|
11680
|
+
case 'h': {
|
|
11681
|
+
const h12 = d.getHours() % 12 || 12;
|
|
11682
|
+
return String(h12);
|
|
11683
|
+
}
|
|
11684
|
+
case 'mm':
|
|
11685
|
+
return pad(d.getMinutes());
|
|
11686
|
+
case 'm':
|
|
11687
|
+
return String(d.getMinutes());
|
|
11688
|
+
case 'ss':
|
|
11689
|
+
return pad(d.getSeconds());
|
|
11690
|
+
case 's':
|
|
11691
|
+
return String(d.getSeconds());
|
|
11692
|
+
case 'A':
|
|
11693
|
+
return d.getHours() < 12 ? 'AM' : 'PM';
|
|
11694
|
+
case 'a':
|
|
11695
|
+
return d.getHours() < 12 ? 'am' : 'pm';
|
|
11696
|
+
default:
|
|
11697
|
+
return token;
|
|
11698
|
+
}
|
|
11699
|
+
}
|
|
11700
|
+
function formatTokenString(d, fmt) {
|
|
11701
|
+
return fmt.replace(TOKEN_RE, t => formatToken(d, t));
|
|
11702
|
+
}
|
|
11703
|
+
function formatWithIntl(d, options, locale) {
|
|
11704
|
+
return new Intl.DateTimeFormat(locale, options).format(d);
|
|
11705
|
+
}
|
|
11706
|
+
function formatDate(d, fmt, locale) {
|
|
11707
|
+
const f = fmt ?? DEFAULT_DATE_FORMAT;
|
|
11708
|
+
if (typeof f === 'string')
|
|
11709
|
+
return formatTokenString(d, f);
|
|
11710
|
+
return formatWithIntl(d, f, locale);
|
|
11711
|
+
}
|
|
11712
|
+
function formatTime(d, fmt, locale) {
|
|
11713
|
+
const f = fmt ?? DEFAULT_TIME_FORMAT_24;
|
|
11714
|
+
if (typeof f === 'string')
|
|
11715
|
+
return formatTokenString(d, f);
|
|
11716
|
+
return formatWithIntl(d, f, locale);
|
|
11717
|
+
}
|
|
11718
|
+
function formatDateTime(d, fmt, locale) {
|
|
11719
|
+
const f = fmt ?? DEFAULT_DATETIME_FORMAT;
|
|
11720
|
+
if (typeof f === 'string')
|
|
11721
|
+
return formatTokenString(d, f);
|
|
11722
|
+
return formatWithIntl(d, f, locale);
|
|
11723
|
+
}
|
|
11724
|
+
/**
|
|
11725
|
+
* Derive the hour cycle (`'12'` or `'24'`) a display format will render, from
|
|
11726
|
+
* its first hour token: `h`/`hh` → 12-hour with meridiem, `H`/`HH` → 24-hour.
|
|
11727
|
+
*
|
|
11728
|
+
* Returns `null` when the cycle can't be read from the format — it's an
|
|
11729
|
+
* `Intl.DateTimeFormat` options object (no token string), `undefined`, or has
|
|
11730
|
+
* no hour token — so callers fall back to the raw `hourFormat` prop. Scans with
|
|
11731
|
+
* the same {@link TOKEN_RE} grammar the input renders with, so the derived cycle
|
|
11732
|
+
* always agrees with what the field actually displays.
|
|
11733
|
+
*/
|
|
11734
|
+
function hourCycleFromFormat(fmt) {
|
|
11735
|
+
if (typeof fmt !== 'string')
|
|
11736
|
+
return null;
|
|
11737
|
+
for (const [token] of fmt.matchAll(TOKEN_RE)) {
|
|
11738
|
+
if (token === 'H' || token === 'HH')
|
|
11739
|
+
return '24';
|
|
11740
|
+
if (token === 'h' || token === 'hh')
|
|
11741
|
+
return '12';
|
|
11742
|
+
}
|
|
11743
|
+
return null;
|
|
11744
|
+
}
|
|
11745
|
+
/**
|
|
11746
|
+
* Parse a date string against a token format. Returns null on mismatch.
|
|
11747
|
+
* For Intl-options formats parsing is the consumer's responsibility — pass
|
|
11748
|
+
* a custom `parse` prop on the picker.
|
|
11749
|
+
*/
|
|
11750
|
+
function parseDate(s, fmt,
|
|
11751
|
+
// locale arg accepted for symmetry; locale-aware parsing is not supported.
|
|
11752
|
+
_locale) {
|
|
11753
|
+
if (!s)
|
|
11754
|
+
return null;
|
|
11755
|
+
const format = fmt ?? DEFAULT_DATE_FORMAT;
|
|
11756
|
+
const parts = parseTokens(s, format);
|
|
11757
|
+
if (!parts)
|
|
11758
|
+
return null;
|
|
11759
|
+
const year = parts.year ?? new Date().getFullYear();
|
|
11760
|
+
const month = (parts.month ?? 1) - 1;
|
|
11761
|
+
const day = parts.day ?? 1;
|
|
11762
|
+
const hours = parts.hours ?? 0;
|
|
11763
|
+
const minutes = parts.minutes ?? 0;
|
|
11764
|
+
const seconds = parts.seconds ?? 0;
|
|
11765
|
+
const d = makeDate(year, month, day);
|
|
11766
|
+
d.setHours(hours, minutes, seconds, 0);
|
|
11767
|
+
if (d.getFullYear() !== year ||
|
|
11768
|
+
d.getMonth() !== month ||
|
|
11769
|
+
d.getDate() !== day) {
|
|
11770
|
+
return null;
|
|
11771
|
+
}
|
|
11772
|
+
return d;
|
|
11773
|
+
}
|
|
11774
|
+
function parseTime(s, fmt) {
|
|
11775
|
+
if (!s)
|
|
11776
|
+
return null;
|
|
11777
|
+
const format = fmt ?? DEFAULT_TIME_FORMAT_24;
|
|
11778
|
+
const parts = parseTokens(s, format);
|
|
11779
|
+
if (!parts)
|
|
11780
|
+
return null;
|
|
11781
|
+
const today = new Date();
|
|
11782
|
+
return new Date(today.getFullYear(), today.getMonth(), today.getDate(), parts.hours ?? 0, parts.minutes ?? 0, parts.seconds ?? 0, 0);
|
|
11783
|
+
}
|
|
11784
|
+
function escapeRegExp(s) {
|
|
11785
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11786
|
+
}
|
|
11787
|
+
function parseTokens(input, fmt) {
|
|
11788
|
+
const tokenOrder = [];
|
|
11789
|
+
let regex = '';
|
|
11790
|
+
let i = 0;
|
|
11791
|
+
while (i < fmt.length) {
|
|
11792
|
+
const four = fmt.slice(i, i + 4);
|
|
11793
|
+
const two = fmt.slice(i, i + 2);
|
|
11794
|
+
const one = fmt[i];
|
|
11795
|
+
let token = null;
|
|
11796
|
+
if (four === 'YYYY')
|
|
11797
|
+
token = 'YYYY';
|
|
11798
|
+
else if (two === 'YY' ||
|
|
11799
|
+
two === 'MM' ||
|
|
11800
|
+
two === 'DD' ||
|
|
11801
|
+
two === 'HH' ||
|
|
11802
|
+
two === 'hh' ||
|
|
11803
|
+
two === 'mm' ||
|
|
11804
|
+
two === 'ss')
|
|
11805
|
+
token = two;
|
|
11806
|
+
else if (one === 'M' ||
|
|
11807
|
+
one === 'D' ||
|
|
11808
|
+
one === 'H' ||
|
|
11809
|
+
one === 'h' ||
|
|
11810
|
+
one === 'm' ||
|
|
11811
|
+
one === 's' ||
|
|
11812
|
+
one === 'A' ||
|
|
11813
|
+
one === 'a')
|
|
11814
|
+
token = one;
|
|
11815
|
+
if (token) {
|
|
11816
|
+
tokenOrder.push(token);
|
|
11817
|
+
switch (token) {
|
|
11818
|
+
case 'YYYY':
|
|
11819
|
+
regex += '(\\d{4})';
|
|
11820
|
+
break;
|
|
11821
|
+
case 'YY':
|
|
11822
|
+
regex += '(\\d{2})';
|
|
11823
|
+
break;
|
|
11824
|
+
case 'MM':
|
|
11825
|
+
case 'DD':
|
|
11826
|
+
case 'HH':
|
|
11827
|
+
case 'hh':
|
|
11828
|
+
case 'mm':
|
|
11829
|
+
case 'ss':
|
|
11830
|
+
regex += '(\\d{1,2})';
|
|
11831
|
+
break;
|
|
11832
|
+
case 'M':
|
|
11833
|
+
case 'D':
|
|
11834
|
+
case 'H':
|
|
11835
|
+
case 'h':
|
|
11836
|
+
case 'm':
|
|
11837
|
+
case 's':
|
|
11838
|
+
regex += '(\\d{1,2})';
|
|
11839
|
+
break;
|
|
11840
|
+
case 'A':
|
|
11841
|
+
regex += '(AM|PM)';
|
|
11842
|
+
break;
|
|
11843
|
+
case 'a':
|
|
11844
|
+
regex += '(am|pm)';
|
|
11845
|
+
break;
|
|
11846
|
+
}
|
|
11847
|
+
i += token.length;
|
|
11848
|
+
}
|
|
11849
|
+
else {
|
|
11850
|
+
regex += escapeRegExp(one);
|
|
11851
|
+
i += 1;
|
|
11704
11852
|
}
|
|
11705
11853
|
}
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11854
|
+
const m = new RegExp('^' + regex + '$').exec(input);
|
|
11855
|
+
if (!m)
|
|
11856
|
+
return null;
|
|
11857
|
+
const out = {};
|
|
11858
|
+
tokenOrder.forEach((token, idx) => {
|
|
11859
|
+
const v = m[idx + 1];
|
|
11860
|
+
switch (token) {
|
|
11861
|
+
case 'YYYY':
|
|
11862
|
+
out.year = Number(v);
|
|
11863
|
+
break;
|
|
11864
|
+
case 'YY':
|
|
11865
|
+
out.year = 2000 + Number(v);
|
|
11866
|
+
break;
|
|
11867
|
+
case 'MM':
|
|
11868
|
+
case 'M':
|
|
11869
|
+
out.month = Number(v);
|
|
11870
|
+
break;
|
|
11871
|
+
case 'DD':
|
|
11872
|
+
case 'D':
|
|
11873
|
+
out.day = Number(v);
|
|
11874
|
+
break;
|
|
11875
|
+
case 'HH':
|
|
11876
|
+
case 'H':
|
|
11877
|
+
out.hours = Number(v);
|
|
11878
|
+
break;
|
|
11879
|
+
case 'hh':
|
|
11880
|
+
case 'h':
|
|
11881
|
+
out.hours = Number(v);
|
|
11882
|
+
break;
|
|
11883
|
+
case 'mm':
|
|
11884
|
+
case 'm':
|
|
11885
|
+
out.minutes = Number(v);
|
|
11886
|
+
break;
|
|
11887
|
+
case 'ss':
|
|
11888
|
+
case 's':
|
|
11889
|
+
out.seconds = Number(v);
|
|
11890
|
+
break;
|
|
11891
|
+
case 'A':
|
|
11892
|
+
out.meridiem = v.toLowerCase() === 'am' ? 'am' : 'pm';
|
|
11893
|
+
break;
|
|
11894
|
+
case 'a':
|
|
11895
|
+
out.meridiem = v === 'am' ? 'am' : 'pm';
|
|
11896
|
+
break;
|
|
11897
|
+
}
|
|
11898
|
+
});
|
|
11899
|
+
if (out.meridiem !== undefined && out.hours !== undefined) {
|
|
11900
|
+
if (out.meridiem === 'pm' && out.hours < 12)
|
|
11901
|
+
out.hours += 12;
|
|
11902
|
+
if (out.meridiem === 'am' && out.hours === 12)
|
|
11903
|
+
out.hours = 0;
|
|
11709
11904
|
}
|
|
11710
|
-
|
|
11711
|
-
return r;
|
|
11905
|
+
return out;
|
|
11712
11906
|
}
|
|
11713
11907
|
/**
|
|
11714
|
-
*
|
|
11715
|
-
*
|
|
11716
|
-
* of the month, where the week starts on `firstDayOfWeek`.
|
|
11908
|
+
* Locale-aware day name labels in calendar order (Sunday → Saturday).
|
|
11909
|
+
* Caller rotates by `firstDayOfWeek`.
|
|
11717
11910
|
*/
|
|
11718
|
-
function
|
|
11719
|
-
const
|
|
11720
|
-
|
|
11721
|
-
const
|
|
11722
|
-
const
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
cells.push({
|
|
11728
|
-
date,
|
|
11729
|
-
inCurrentMonth: date.getMonth() === monthAnchor.getMonth(),
|
|
11730
|
-
isToday: isSameDay(date, today),
|
|
11731
|
-
});
|
|
11911
|
+
function getDayNames(locale, length = 'short') {
|
|
11912
|
+
const fmt = new Intl.DateTimeFormat(locale, { weekday: length });
|
|
11913
|
+
// 2021-01-03 was a Sunday; use it as the anchor.
|
|
11914
|
+
const sunday = new Date(2021, 0, 3);
|
|
11915
|
+
const out = [];
|
|
11916
|
+
for (let i = 0; i < 7; i++) {
|
|
11917
|
+
const d = new Date(sunday);
|
|
11918
|
+
d.setDate(sunday.getDate() + i);
|
|
11919
|
+
out.push(fmt.format(d));
|
|
11732
11920
|
}
|
|
11733
|
-
return
|
|
11921
|
+
return out;
|
|
11922
|
+
}
|
|
11923
|
+
function getMonthNames(locale, length = 'long') {
|
|
11924
|
+
const fmt = new Intl.DateTimeFormat(locale, { month: length });
|
|
11925
|
+
const out = [];
|
|
11926
|
+
for (let i = 0; i < 12; i++) {
|
|
11927
|
+
out.push(fmt.format(new Date(2021, i, 1)));
|
|
11928
|
+
}
|
|
11929
|
+
return out;
|
|
11734
11930
|
}
|
|
11735
11931
|
|
|
11932
|
+
/**
|
|
11933
|
+
* Selects the calendar's focused cell, the one tab stop of whichever grid or
|
|
11934
|
+
* year list is on show, for a popover to put focus on as it opens. It runs
|
|
11935
|
+
* against the whole panel, so it matches only inside the grid or the list.
|
|
11936
|
+
*/
|
|
11937
|
+
const CALENDAR_FOCUSED_CELL = '[role="grid"] [data-focused="true"], [role="listbox"] [data-focused-year="true"]';
|
|
11736
11938
|
const BASE_VIEW = {
|
|
11737
11939
|
day: 'days',
|
|
11738
11940
|
month: 'months',
|
|
@@ -11778,20 +11980,32 @@ function monthStep(key, col) {
|
|
|
11778
11980
|
}
|
|
11779
11981
|
}
|
|
11780
11982
|
/**
|
|
11781
|
-
*
|
|
11782
|
-
*
|
|
11783
|
-
*
|
|
11983
|
+
* Whether DOM focus should follow a grid's tab stop as it moves: focus is in
|
|
11984
|
+
* the grid, or fell to <body> while the grid held it last, as it does when
|
|
11985
|
+
* the focused cell leaves the page or becomes disabled. Focus is read in the
|
|
11986
|
+
* grid's own tree, so this holds inside a shadow root too.
|
|
11784
11987
|
*/
|
|
11785
|
-
function
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
|
|
11988
|
+
function focusFollows(grid, heldLast) {
|
|
11989
|
+
const active = getActiveElementInTree(grid);
|
|
11990
|
+
if (grid.contains(active))
|
|
11991
|
+
return true;
|
|
11992
|
+
return heldLast && active === grid.ownerDocument.body;
|
|
11993
|
+
}
|
|
11994
|
+
/**
|
|
11995
|
+
* The cell nearest index `i` that `disabled` leaves enabled, `i` itself when
|
|
11996
|
+
* it is. At equal distance the later cell wins. With every cell disabled it
|
|
11997
|
+
* stays `i`, as there is nothing to reach.
|
|
11998
|
+
*/
|
|
11999
|
+
function nearestEnabled(i, disabled) {
|
|
12000
|
+
if (!disabled[i])
|
|
12001
|
+
return i;
|
|
12002
|
+
for (let d = 1; d < disabled.length; d++) {
|
|
12003
|
+
if (i + d < disabled.length && !disabled[i + d])
|
|
12004
|
+
return i + d;
|
|
12005
|
+
if (i - d >= 0 && !disabled[i - d])
|
|
12006
|
+
return i - d;
|
|
11793
12007
|
}
|
|
11794
|
-
return
|
|
12008
|
+
return i;
|
|
11795
12009
|
}
|
|
11796
12010
|
/**
|
|
11797
12011
|
* The year list's keyboard when it is the selection surface: arrows move a
|
|
@@ -11817,7 +12031,7 @@ function yearStep(key, focusedYear, years) {
|
|
|
11817
12031
|
return null;
|
|
11818
12032
|
}
|
|
11819
12033
|
}
|
|
11820
|
-
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', }) => {
|
|
12034
|
+
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', }) => {
|
|
11821
12035
|
const gridRef = React.useRef(null);
|
|
11822
12036
|
const monthGridRef = React.useRef(null);
|
|
11823
12037
|
const yearGridRef = React.useRef(null);
|
|
@@ -11827,6 +12041,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11827
12041
|
const isDayGranularity = granularity === 'day';
|
|
11828
12042
|
const isYearGranularity = granularity === 'year';
|
|
11829
12043
|
const [view, setView] = React.useState(baseView);
|
|
12044
|
+
// Nothing before year 1 can be picked or reached, as in HTML's date inputs.
|
|
12045
|
+
const min = React.useMemo(() => floorMin(minProp), [minProp]);
|
|
11830
12046
|
const computedDayNames = React.useMemo(() => {
|
|
11831
12047
|
if (dayNames && dayNames.length === 7)
|
|
11832
12048
|
return dayNames;
|
|
@@ -11863,6 +12079,33 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11863
12079
|
shouldDisableDate,
|
|
11864
12080
|
unselectableDates,
|
|
11865
12081
|
}), [min, max, shouldDisableDate, unselectableDates]);
|
|
12082
|
+
// The day grid's one tab stop. A disabled button can't take focus, so when
|
|
12083
|
+
// the focused day is disabled the stop moves to the nearest enabled day of
|
|
12084
|
+
// the month on show, and focusing it makes it the focused day. A nearby
|
|
12085
|
+
// month's day is left out, as focusing it would turn the grid to its month.
|
|
12086
|
+
const tabStopDay = React.useMemo(() => {
|
|
12087
|
+
const days = cells.filter(c => c.inCurrentMonth).map(c => c.date);
|
|
12088
|
+
const i = nearestEnabled(focusedDate.getDate() - 1, days.map(isDateUnselectable));
|
|
12089
|
+
return days[i];
|
|
12090
|
+
}, [cells, focusedDate, isDateUnselectable]);
|
|
12091
|
+
// Whether the grid on show holds focus, or held it last before focus went
|
|
12092
|
+
// nowhere: the cell that had it left the page or became disabled. A blur
|
|
12093
|
+
// that takes focus out of the grid, to nowhere included, is the user
|
|
12094
|
+
// leaving, so it clears this. A render that removes or disables the focused
|
|
12095
|
+
// cell sends no blur here: browsers fire none, or fire it inside React's
|
|
12096
|
+
// commit, where React delivers no events.
|
|
12097
|
+
const gridHeldFocusRef = React.useRef(false);
|
|
12098
|
+
const gridFocusHandlers = {
|
|
12099
|
+
onFocus: () => {
|
|
12100
|
+
gridHeldFocusRef.current = true;
|
|
12101
|
+
},
|
|
12102
|
+
onBlur: (e) => {
|
|
12103
|
+
const next = e.relatedTarget;
|
|
12104
|
+
if (!next || !e.currentTarget.contains(next)) {
|
|
12105
|
+
gridHeldFocusRef.current = false;
|
|
12106
|
+
}
|
|
12107
|
+
},
|
|
12108
|
+
};
|
|
11866
12109
|
// Step from `next` by `direction` days until we hit a selectable date,
|
|
11867
12110
|
// skipping past dates blocked by min/max, shouldDisableDate, or
|
|
11868
12111
|
// unselectableDates. If the whole searched range is disabled, focus stays
|
|
@@ -11870,7 +12113,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11870
12113
|
const moveFocus = React.useCallback((next, direction) => {
|
|
11871
12114
|
let candidate = next;
|
|
11872
12115
|
for (let i = 0; i < 366; i++) {
|
|
11873
|
-
if (
|
|
12116
|
+
if (candidate.getTime() < startOfDay(min).getTime())
|
|
11874
12117
|
return;
|
|
11875
12118
|
if (max && candidate.getTime() > startOfDay(max).getTime())
|
|
11876
12119
|
return;
|
|
@@ -11936,7 +12179,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11936
12179
|
let candidate = next;
|
|
11937
12180
|
for (let i = 0; i < 120; i++) {
|
|
11938
12181
|
const month = startOfMonth(candidate).getTime();
|
|
11939
|
-
if (
|
|
12182
|
+
if (month < startOfMonth(min).getTime())
|
|
11940
12183
|
return;
|
|
11941
12184
|
if (max && month > startOfMonth(max).getTime())
|
|
11942
12185
|
return;
|
|
@@ -11984,25 +12227,22 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11984
12227
|
const today = new Date();
|
|
11985
12228
|
const todayYear = today.getFullYear();
|
|
11986
12229
|
// The header steps a month at a time over the day grid and a year at a
|
|
11987
|
-
// time over the month grid, kept inside min/max.
|
|
11988
|
-
|
|
11989
|
-
|
|
12230
|
+
// time over the month grid, kept inside min/max. A step is off once the
|
|
12231
|
+
// month or year on show holds the bound or lies past it, as picking a year
|
|
12232
|
+
// from the list can leave it.
|
|
11990
12233
|
const prevAnchor = isDayGranularity
|
|
11991
|
-
?
|
|
12234
|
+
? addMonths(focusedDate, -1)
|
|
11992
12235
|
: clampDate(addYears(focusedDate, -1), min, max);
|
|
11993
12236
|
const nextAnchor = isDayGranularity
|
|
11994
|
-
?
|
|
12237
|
+
? addMonths(focusedDate, 1)
|
|
11995
12238
|
: clampDate(addYears(focusedDate, 1), min, max);
|
|
12239
|
+
const focusedMonth = startOfMonth(focusedDate).getTime();
|
|
11996
12240
|
const prevDisabled = isDayGranularity
|
|
11997
|
-
?
|
|
11998
|
-
|
|
11999
|
-
isSameMonth(focusedDate, min))
|
|
12000
|
-
: !!(min && focusedYear <= min.getFullYear());
|
|
12241
|
+
? focusedMonth <= startOfMonth(min).getTime()
|
|
12242
|
+
: focusedYear <= min.getFullYear();
|
|
12001
12243
|
const nextDisabled = isDayGranularity
|
|
12002
|
-
? !!
|
|
12003
|
-
|
|
12004
|
-
isSameMonth(focusedDate, max))
|
|
12005
|
-
: !!(max && focusedYear >= max.getFullYear());
|
|
12244
|
+
? !!max && focusedMonth >= startOfMonth(max).getTime()
|
|
12245
|
+
: !!max && focusedYear >= max.getFullYear();
|
|
12006
12246
|
const labelId = id ? `${id}-label` : undefined;
|
|
12007
12247
|
const monthLabel = isDayGranularity
|
|
12008
12248
|
? `${computedMonthNames[focusedDate.getMonth()]} ${focusedYear}`
|
|
@@ -12013,7 +12253,9 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12013
12253
|
const [mountYear] = React.useState(focusedYear);
|
|
12014
12254
|
const windowYear = isYearGranularity ? mountYear : focusedYear;
|
|
12015
12255
|
const yearList = React.useMemo(() => {
|
|
12016
|
-
const minYear =
|
|
12256
|
+
const minYear = minProp
|
|
12257
|
+
? min.getFullYear()
|
|
12258
|
+
: Math.max(FIRST_YEAR, windowYear - 100);
|
|
12017
12259
|
const maxYear = max ? max.getFullYear() : windowYear + 100;
|
|
12018
12260
|
const [lo, hi] = yearsRange ?? [minYear, maxYear];
|
|
12019
12261
|
const start = Math.max(lo, minYear);
|
|
@@ -12022,10 +12264,25 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12022
12264
|
for (let y = start; y <= end; y++)
|
|
12023
12265
|
out.push(y);
|
|
12024
12266
|
return out;
|
|
12025
|
-
}, [windowYear, min, max, yearsRange]);
|
|
12267
|
+
}, [windowYear, minProp, min, max, yearsRange]);
|
|
12026
12268
|
// Only the selection surface disables years; as navigation every listed
|
|
12027
12269
|
// year can be visited.
|
|
12028
12270
|
const disabledYears = React.useMemo(() => new Set(isYearGranularity ? yearList.filter(y => isYearUnselectable(y)) : []), [isYearGranularity, yearList, isYearUnselectable]);
|
|
12271
|
+
// The year list's one tab stop, after the month grid's: when the focused
|
|
12272
|
+
// year is disabled the stop moves to the nearest enabled year in the list,
|
|
12273
|
+
// and focusing it makes it the focused year. As the selection surface a
|
|
12274
|
+
// year outside a non-empty list starts from the end it fell past, so the
|
|
12275
|
+
// list keeps a stop. As navigation the stop marks the current year, so a
|
|
12276
|
+
// year outside the list marks no option.
|
|
12277
|
+
const tabStopYear = React.useMemo(() => {
|
|
12278
|
+
let i = yearList.indexOf(focusedYear);
|
|
12279
|
+
if (i < 0) {
|
|
12280
|
+
if (!isYearGranularity || yearList.length === 0)
|
|
12281
|
+
return focusedYear;
|
|
12282
|
+
i = focusedYear < yearList[0] ? 0 : yearList.length - 1;
|
|
12283
|
+
}
|
|
12284
|
+
return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
|
|
12285
|
+
}, [yearList, focusedYear, disabledYears, isYearGranularity]);
|
|
12029
12286
|
// Which of the focused year's months have no selectable day.
|
|
12030
12287
|
const disabledMonths = React.useMemo(() => granularity === 'month'
|
|
12031
12288
|
? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
|
|
@@ -12033,7 +12290,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12033
12290
|
// The month grid's one tab stop. A disabled button can't take focus, so
|
|
12034
12291
|
// when the focused month is disabled the stop moves to the nearest enabled
|
|
12035
12292
|
// month, and focusing it makes it the focused month.
|
|
12036
|
-
const tabStopMonth =
|
|
12293
|
+
const tabStopMonth = nearestEnabled(focusedDate.getMonth(), disabledMonths);
|
|
12037
12294
|
const handleYearSelect = React.useCallback((year) => {
|
|
12038
12295
|
const next = new Date(focusedDate);
|
|
12039
12296
|
next.setFullYear(year);
|
|
@@ -12075,24 +12332,37 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12075
12332
|
e.preventDefault();
|
|
12076
12333
|
moveYearFocus(step[0], step[1]);
|
|
12077
12334
|
}, [isYearGranularity, baseView, focusedYear, yearList, moveYearFocus]);
|
|
12335
|
+
// The day or month grid takes focus as the popover opens on it, or comes
|
|
12336
|
+
// back to it from the year list.
|
|
12337
|
+
React.useEffect(() => {
|
|
12338
|
+
if (!autoFocusCell)
|
|
12339
|
+
return;
|
|
12340
|
+
const grid = view === 'days'
|
|
12341
|
+
? gridRef.current
|
|
12342
|
+
: view === 'months'
|
|
12343
|
+
? monthGridRef.current
|
|
12344
|
+
: null;
|
|
12345
|
+
grid?.querySelector('[data-focused="true"]')?.focus();
|
|
12346
|
+
}, [autoFocusCell, view]);
|
|
12347
|
+
// From then on focus follows the tab stop only while the grid has it, or
|
|
12348
|
+
// has just lost it, so focus on a header button or a time wheel stays put.
|
|
12349
|
+
const tabStopDayTime = tabStopDay.getTime();
|
|
12078
12350
|
React.useEffect(() => {
|
|
12079
|
-
|
|
12351
|
+
const grid = gridRef.current;
|
|
12352
|
+
if (view !== 'days' || !grid)
|
|
12080
12353
|
return;
|
|
12081
|
-
if (!
|
|
12354
|
+
if (!focusFollows(grid, gridHeldFocusRef.current))
|
|
12082
12355
|
return;
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
}, [autoFocusCell, focusedDate, view]);
|
|
12086
|
-
// The month grid takes focus when the popover asks for it, and keeps it on
|
|
12087
|
-
// the focused month while the keyboard moves through the grid.
|
|
12356
|
+
grid.querySelector('[data-focused="true"]')?.focus();
|
|
12357
|
+
}, [tabStopDayTime, view]);
|
|
12088
12358
|
React.useEffect(() => {
|
|
12089
12359
|
const grid = monthGridRef.current;
|
|
12090
12360
|
if (view !== 'months' || !grid)
|
|
12091
12361
|
return;
|
|
12092
|
-
if (!
|
|
12362
|
+
if (!focusFollows(grid, gridHeldFocusRef.current))
|
|
12093
12363
|
return;
|
|
12094
12364
|
grid.querySelector('[data-focused="true"]')?.focus();
|
|
12095
|
-
}, [
|
|
12365
|
+
}, [focusedYear, tabStopMonth, view]);
|
|
12096
12366
|
// When the year view opens as navigation, scroll the focused year into
|
|
12097
12367
|
// view.
|
|
12098
12368
|
React.useEffect(() => {
|
|
@@ -12126,15 +12396,15 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12126
12396
|
const list = yearGridRef.current;
|
|
12127
12397
|
if (!isYearGranularity || !list)
|
|
12128
12398
|
return;
|
|
12129
|
-
if (!list.contains(
|
|
12399
|
+
if (!list.contains(getActiveElementInTree(list)))
|
|
12130
12400
|
return;
|
|
12131
12401
|
list.querySelector('[data-focused-year="true"]')?.focus();
|
|
12132
12402
|
}, [isYearGranularity, focusedYear]);
|
|
12133
12403
|
const navArrow = (d) => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: d, fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
12134
|
-
return (jsxRuntime.jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxRuntime.jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxRuntime.jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => setView(v => (v === 'years' ? baseView : 'years')), children: [jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsxRuntime.jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxRuntime.jsxs("div", { className: navGroupClass, children: [jsxRuntime.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') }), jsxRuntime.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' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsxRuntime.jsx("div", { className: dayNameClass, children: name }, i))) }), jsxRuntime.jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, children: cells.map(cell => {
|
|
12404
|
+
return (jsxRuntime.jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxRuntime.jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxRuntime.jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => setView(v => (v === 'years' ? baseView : 'years')), children: [jsxRuntime.jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsxRuntime.jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxRuntime.jsxs("div", { className: navGroupClass, children: [jsxRuntime.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') }), jsxRuntime.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' && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsxRuntime.jsx("div", { className: dayNameClass, children: name }, i))) }), jsxRuntime.jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
|
|
12135
12405
|
const disabled = isDateUnselectable(cell.date);
|
|
12136
12406
|
const isSelected = !!value && isSameDay(value, cell.date);
|
|
12137
|
-
const isFocused = isSameDay(cell.date,
|
|
12407
|
+
const isFocused = isSameDay(cell.date, tabStopDay);
|
|
12138
12408
|
const otherMonth = !cell.inCurrentMonth;
|
|
12139
12409
|
const cellClass = prefixedClassNames(classPrefix, 'dateinput-cell', {
|
|
12140
12410
|
'is-selected': isSelected,
|
|
@@ -12143,13 +12413,20 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12143
12413
|
'is-other-month': otherMonth,
|
|
12144
12414
|
});
|
|
12145
12415
|
const display = !otherMonth || nearbyMonthDays;
|
|
12146
|
-
return (jsxRuntime.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,
|
|
12416
|
+
return (jsxRuntime.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: () => {
|
|
12417
|
+
// Reached by Tab while the focused day is disabled, or
|
|
12418
|
+
// by pointer: keys move on from here. A nearby month's
|
|
12419
|
+
// day waits for its click, which turns the grid.
|
|
12420
|
+
if (!otherMonth && !isSameDay(cell.date, focusedDate)) {
|
|
12421
|
+
onFocusedDateChange(cell.date);
|
|
12422
|
+
}
|
|
12423
|
+
}, onClick: () => {
|
|
12147
12424
|
if (disabled)
|
|
12148
12425
|
return;
|
|
12149
12426
|
onFocusedDateChange(cell.date);
|
|
12150
12427
|
onSelect(cell.date);
|
|
12151
12428
|
}, style: !display ? { visibility: 'hidden' } : undefined, children: cell.date.getDate() }, cell.date.toISOString()));
|
|
12152
|
-
}) })] })), view === 'months' && (jsxRuntime.jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, children: MONTH_ROWS.map(row => (jsxRuntime.jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
|
|
12429
|
+
}) })] })), view === 'months' && (jsxRuntime.jsx("div", { ref: monthGridRef, role: "grid", "aria-labelledby": labelId, className: monthsGridClass, onKeyDown: handleMonthKeyDown, ...gridFocusHandlers, children: MONTH_ROWS.map(row => (jsxRuntime.jsx("div", { role: "row", className: monthsRowClass, children: row.map(month => {
|
|
12153
12430
|
const date = makeDate(focusedYear, month);
|
|
12154
12431
|
const disabled = disabledMonths[month];
|
|
12155
12432
|
const isSelected = !!value && isSameMonth(value, date);
|
|
@@ -12171,7 +12448,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12171
12448
|
onSelect(date);
|
|
12172
12449
|
}, children: monthCellNames[month] }, month));
|
|
12173
12450
|
}) }, row[0]))) })), view === 'years' && (jsxRuntime.jsx("div", { ref: yearGridRef, className: yearsGridClass, role: "listbox", "aria-label": isYearGranularity ? t.chooseYear : monthLabel, onKeyDown: handleYearListKeyDown, children: yearList.map(year => {
|
|
12174
|
-
const isFocused = year ===
|
|
12451
|
+
const isFocused = year === tabStopYear;
|
|
12175
12452
|
const isToday = year === todayYear;
|
|
12176
12453
|
// As navigation the focused year is the current one. As the
|
|
12177
12454
|
// selection surface the value's year is selected and focus roams.
|
|
@@ -12184,7 +12461,13 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12184
12461
|
'is-today': isToday,
|
|
12185
12462
|
'is-disabled': disabled,
|
|
12186
12463
|
});
|
|
12187
|
-
return (jsxRuntime.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,
|
|
12464
|
+
return (jsxRuntime.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: () => {
|
|
12465
|
+
// Reached by Tab while the focused year is disabled, or by
|
|
12466
|
+
// pointer: keys move on from here.
|
|
12467
|
+
if (isYearGranularity && year !== focusedYear) {
|
|
12468
|
+
onFocusedDateChange(clampDate(makeDate(year), min, max));
|
|
12469
|
+
}
|
|
12470
|
+
}, onClick: () => isYearGranularity
|
|
12188
12471
|
? handleYearPick(year)
|
|
12189
12472
|
: handleYearSelect(year), children: year }, year));
|
|
12190
12473
|
}) }))] }));
|
|
@@ -12192,12 +12475,25 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12192
12475
|
Calendar.displayName = 'Calendar';
|
|
12193
12476
|
|
|
12194
12477
|
const isBrowser = typeof window !== 'undefined';
|
|
12195
|
-
const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'bottom-left', appendToBody = false, className, trapFocus = true, closeOnClickOutside = true, closeOnEscape = true, ariaLabel, ariaLabelledBy, role = 'dialog', id, restoreFocusRef, }) => {
|
|
12478
|
+
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, }) => {
|
|
12196
12479
|
const panelRef = React.useRef(null);
|
|
12480
|
+
// Read as the trap turns on, once the panel's content has rendered.
|
|
12481
|
+
const initialFocusRef = React.useMemo(() => initialFocusSelector === undefined
|
|
12482
|
+
? undefined
|
|
12483
|
+
: {
|
|
12484
|
+
get current() {
|
|
12485
|
+
return (panelRef.current?.querySelector(initialFocusSelector) ?? null);
|
|
12486
|
+
},
|
|
12487
|
+
}, [initialFocusSelector]);
|
|
12197
12488
|
const resolved = useAnchoredPosition(anchorRef, panelRef, {
|
|
12198
12489
|
active: isOpen,
|
|
12199
12490
|
position,
|
|
12200
12491
|
fixed: appendToBody,
|
|
12492
|
+
// The gap comes from `--bulma-picker-popover-offset` as a margin, in place
|
|
12493
|
+
// or portaled, so the coordinates leave it out and `auto` reads it off
|
|
12494
|
+
// the panel.
|
|
12495
|
+
offset: 0,
|
|
12496
|
+
styledGap: marginGap,
|
|
12201
12497
|
});
|
|
12202
12498
|
React.useEffect(() => {
|
|
12203
12499
|
if (!isOpen || !closeOnClickOutside || !isBrowser)
|
|
@@ -12231,6 +12527,7 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
|
|
|
12231
12527
|
}, [isOpen, closeOnEscape, onClose]);
|
|
12232
12528
|
useFocusTrap(panelRef, {
|
|
12233
12529
|
active: isOpen && trapFocus,
|
|
12530
|
+
initialFocusRef,
|
|
12234
12531
|
restoreFocus: restoreFocusRef ?? true,
|
|
12235
12532
|
});
|
|
12236
12533
|
const panelClass = usePrefixedClassNames('picker-popover', {
|
|
@@ -12410,8 +12707,11 @@ function buildSegmentMap(format) {
|
|
|
12410
12707
|
// ---------------------------------------------------------------------------
|
|
12411
12708
|
const wrap$1 = (n, max) => ((n % max) + max) % max;
|
|
12412
12709
|
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
|
12413
|
-
/**
|
|
12414
|
-
|
|
12710
|
+
/**
|
|
12711
|
+
* Number of days in the month of `d` (handles leap Februarys). Built on
|
|
12712
|
+
* `makeDate`, so a year below 100 keeps its own leap rule.
|
|
12713
|
+
*/
|
|
12714
|
+
const daysInMonth = (d) => makeDate(d.getFullYear(), d.getMonth() + 1, 0).getDate();
|
|
12415
12715
|
/**
|
|
12416
12716
|
* Returns a new Date with the given segment incremented (`delta` = +1 or -1).
|
|
12417
12717
|
* Wraps at segment boundaries: year is unbounded, month 0↔11 (in place, no
|
|
@@ -12638,33 +12938,35 @@ function useSegmentedEntry(params) {
|
|
|
12638
12938
|
// within the same event tick.
|
|
12639
12939
|
const typedDigitsRef = React.useRef('');
|
|
12640
12940
|
// The text focus seeded an empty picker with, until the user edits it or
|
|
12641
|
-
// leaves.
|
|
12941
|
+
// leaves.
|
|
12642
12942
|
const seedRef = React.useRef(null);
|
|
12643
|
-
// Whether the
|
|
12644
|
-
//
|
|
12645
|
-
|
|
12943
|
+
// Whether the text is still what the picker put there, so nothing in it was
|
|
12944
|
+
// typed: the value's display, or an empty picker's seed until an edit drops
|
|
12945
|
+
// it. Against a value it compares text alone, so typing that ends back on
|
|
12946
|
+
// the display counts as untouched.
|
|
12947
|
+
const isUntouched = React.useCallback((s) => value
|
|
12948
|
+
? s === formatFn(value, format, locale)
|
|
12949
|
+
: seedRef.current !== null && s === seedRef.current, [value, formatFn, format, locale]);
|
|
12646
12950
|
// A seed the popover never turned into a value closes with it, wherever
|
|
12647
12951
|
// focus goes, so a dismissed empty field stays empty.
|
|
12648
12952
|
//
|
|
12649
12953
|
// As the popover closes, its focus trap hands focus back to the input. That
|
|
12650
12954
|
// focus is not the user arriving: it must not open the popover again under
|
|
12651
|
-
// `openOnFocus`, or seed an empty picker
|
|
12652
|
-
//
|
|
12653
|
-
//
|
|
12654
|
-
//
|
|
12655
|
-
// in between is the one handed back.
|
|
12955
|
+
// `openOnFocus`, or seed an empty picker. `closingRef` is set in the layout
|
|
12956
|
+
// phase of the commit that closes it and cleared by that commit's effects,
|
|
12957
|
+
// and React runs a commit's effect cleanups, the trap's among them, before
|
|
12958
|
+
// any of its effects, so a focus in between is the one handed back.
|
|
12656
12959
|
const closingRef = React.useRef(false);
|
|
12657
12960
|
const wasOpenRef = React.useRef(isOpen);
|
|
12658
12961
|
React.useLayoutEffect(() => {
|
|
12659
12962
|
if (wasOpenRef.current && !isOpen) {
|
|
12660
12963
|
closingRef.current = true;
|
|
12661
|
-
if (!value &&
|
|
12964
|
+
if (!value && isUntouched(text))
|
|
12662
12965
|
setText('');
|
|
12663
|
-
}
|
|
12664
12966
|
seedRef.current = null;
|
|
12665
12967
|
}
|
|
12666
12968
|
wasOpenRef.current = isOpen;
|
|
12667
|
-
}, [isOpen, value, text, setText]);
|
|
12969
|
+
}, [isOpen, value, isUntouched, text, setText]);
|
|
12668
12970
|
React.useEffect(() => {
|
|
12669
12971
|
closingRef.current = false;
|
|
12670
12972
|
}, [isOpen]);
|
|
@@ -12742,13 +13044,18 @@ function useSegmentedEntry(params) {
|
|
|
12742
13044
|
}
|
|
12743
13045
|
setActiveSegmentIdx(null);
|
|
12744
13046
|
typedDigitsRef.current = '';
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
//
|
|
12750
|
-
//
|
|
12751
|
-
|
|
13047
|
+
// Leaving commits only what was typed. Re-parsing the value's display
|
|
13048
|
+
// could drop what the format leaves out, such as seconds, and a seed
|
|
13049
|
+
// nobody typed over is not the user's date.
|
|
13050
|
+
const untouched = isUntouched(text);
|
|
13051
|
+
// An open popover keeps an unused seed until it closes, so one portaled
|
|
13052
|
+
// outside the picker, whose focus reads as leaving, keeps it as one
|
|
13053
|
+
// inside does.
|
|
13054
|
+
if (!isOpen)
|
|
13055
|
+
seedRef.current = null;
|
|
13056
|
+
if (untouched) {
|
|
13057
|
+
if (!value && !isOpen)
|
|
13058
|
+
setText('');
|
|
12752
13059
|
onBlur?.(e);
|
|
12753
13060
|
return;
|
|
12754
13061
|
}
|
|
@@ -12762,6 +13069,8 @@ function useSegmentedEntry(params) {
|
|
|
12762
13069
|
onBlur?.(e);
|
|
12763
13070
|
}, [
|
|
12764
13071
|
containerRef,
|
|
13072
|
+
isUntouched,
|
|
13073
|
+
isOpen,
|
|
12765
13074
|
tryParse,
|
|
12766
13075
|
text,
|
|
12767
13076
|
isAllowed,
|
|
@@ -12775,7 +13084,6 @@ function useSegmentedEntry(params) {
|
|
|
12775
13084
|
]);
|
|
12776
13085
|
const handleFocus = React.useCallback((e) => {
|
|
12777
13086
|
const handedBack = closingRef.current;
|
|
12778
|
-
handedBackRef.current = handedBack;
|
|
12779
13087
|
if (!handedBack && openOnFocus && popover && !disabled && !readOnly) {
|
|
12780
13088
|
setOpen(true);
|
|
12781
13089
|
}
|
|
@@ -12943,12 +13251,20 @@ function useSegmentedEntry(params) {
|
|
|
12943
13251
|
}
|
|
12944
13252
|
if (e.key === 'Enter') {
|
|
12945
13253
|
e.preventDefault();
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
13254
|
+
// Untouched text has nothing to commit, as on leaving, but Enter
|
|
13255
|
+
// still confirms what the field shows.
|
|
13256
|
+
if (isUntouched(text)) {
|
|
12949
13257
|
if (closeOnSelect)
|
|
12950
13258
|
setOpen(false);
|
|
12951
13259
|
}
|
|
13260
|
+
else {
|
|
13261
|
+
const parsed = tryParse(text);
|
|
13262
|
+
if (parsed && isAllowed(parsed)) {
|
|
13263
|
+
commitValue(parsed);
|
|
13264
|
+
if (closeOnSelect)
|
|
13265
|
+
setOpen(false);
|
|
13266
|
+
}
|
|
13267
|
+
}
|
|
12952
13268
|
}
|
|
12953
13269
|
onKeyDown?.(e);
|
|
12954
13270
|
}, [
|
|
@@ -12962,6 +13278,7 @@ function useSegmentedEntry(params) {
|
|
|
12962
13278
|
setOpen,
|
|
12963
13279
|
closeOnSelect,
|
|
12964
13280
|
popover,
|
|
13281
|
+
isUntouched,
|
|
12965
13282
|
tryParse,
|
|
12966
13283
|
text,
|
|
12967
13284
|
isAllowed,
|
|
@@ -13010,25 +13327,31 @@ const pad2$1 = (n) => String(n).padStart(2, '0');
|
|
|
13010
13327
|
* The value as the native input and the hidden form input carry it:
|
|
13011
13328
|
* `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
|
|
13012
13329
|
* `<input type="month">` use. The year is padded to four digits, as those
|
|
13013
|
-
* inputs require and as the `YYYY` token displays it.
|
|
13330
|
+
* inputs require and as the `YYYY` token displays it. HTML has no such shape
|
|
13331
|
+
* for a year before 1, so a date then is empty, as those inputs would make it.
|
|
13014
13332
|
*/
|
|
13015
13333
|
const toIsoValue = (d, granularity) => {
|
|
13334
|
+
if (d.getFullYear() < FIRST_YEAR)
|
|
13335
|
+
return '';
|
|
13016
13336
|
const year = String(d.getFullYear()).padStart(4, '0');
|
|
13017
13337
|
if (granularity === 'year')
|
|
13018
13338
|
return year;
|
|
13019
13339
|
const month = `${year}-${pad2$1(d.getMonth() + 1)}`;
|
|
13020
13340
|
return granularity === 'month' ? month : `${month}-${pad2$1(d.getDate())}`;
|
|
13021
13341
|
};
|
|
13022
|
-
/**
|
|
13342
|
+
/**
|
|
13343
|
+
* Read a native `type="date"` or `type="month"` value back into a Date. HTML
|
|
13344
|
+
* allows a year of four or more digits, as `toIsoValue` writes one past 9999.
|
|
13345
|
+
*/
|
|
13023
13346
|
const fromIsoValue = (s, granularity) => {
|
|
13024
13347
|
if (granularity === 'month') {
|
|
13025
|
-
const m = /^(\d{4})-(\d{2})$/.exec(s);
|
|
13348
|
+
const m = /^(\d{4,})-(\d{2})$/.exec(s);
|
|
13026
13349
|
return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
|
|
13027
13350
|
}
|
|
13028
|
-
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
13351
|
+
const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
|
|
13029
13352
|
if (!m)
|
|
13030
13353
|
return null;
|
|
13031
|
-
return
|
|
13354
|
+
return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
13032
13355
|
};
|
|
13033
13356
|
const DEFAULT_FORMATS = {
|
|
13034
13357
|
day: DEFAULT_DATE_FORMAT,
|
|
@@ -13050,14 +13373,19 @@ const SKIPPED_SEGMENTS = {
|
|
|
13050
13373
|
* @returns {JSX.Element}
|
|
13051
13374
|
*/
|
|
13052
13375
|
const DateInputBase = React.forwardRef((props, ref) => {
|
|
13053
|
-
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
|
|
13376
|
+
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;
|
|
13377
|
+
// The launcher gives way to the loading spinner of a Control this sits in.
|
|
13378
|
+
const controlLoading = useControlLoading();
|
|
13379
|
+
const triggerIcon = triggerIconProp ?? !controlLoading;
|
|
13054
13380
|
const t = mergeLabels(labels);
|
|
13055
13381
|
const isDayGranularity = granularity === 'day';
|
|
13056
13382
|
const resolvedFormat = format ?? DEFAULT_FORMATS[granularity];
|
|
13057
13383
|
const isControlled = controlledValue !== undefined;
|
|
13058
13384
|
const [internalValue, setInternalValue] = React.useState(defaultValue ?? null);
|
|
13059
13385
|
const value = isControlled ? (controlledValue ?? null) : internalValue;
|
|
13060
|
-
|
|
13386
|
+
// Nothing before year 1 is in range, as in HTML's date inputs.
|
|
13387
|
+
const lowerBound = React.useMemo(() => floorMin(min), [min]);
|
|
13388
|
+
const initialFocused = React.useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
|
|
13061
13389
|
// intentionally only on mount: keep focusedDate stable until value/open change
|
|
13062
13390
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
13063
13391
|
[]);
|
|
@@ -13065,8 +13393,8 @@ const DateInputBase = React.forwardRef((props, ref) => {
|
|
|
13065
13393
|
// Re-clamp focusedDate if min/max change after mount so the focused cell
|
|
13066
13394
|
// never disappears outside the displayable range.
|
|
13067
13395
|
React.useEffect(() => {
|
|
13068
|
-
setFocusedDate(prev => clampDate(prev,
|
|
13069
|
-
}, [
|
|
13396
|
+
setFocusedDate(prev => clampDate(prev, lowerBound, max));
|
|
13397
|
+
}, [lowerBound, max]);
|
|
13070
13398
|
const [open, setOpenState] = React.useState(false);
|
|
13071
13399
|
const [text, setText] = React.useState(value ? formatDate(value, resolvedFormat, locale) : '');
|
|
13072
13400
|
const inputRef = React.useRef(null);
|
|
@@ -13092,16 +13420,20 @@ const DateInputBase = React.forwardRef((props, ref) => {
|
|
|
13092
13420
|
});
|
|
13093
13421
|
const containerClass = usePrefixedClassNames('dateinput-container');
|
|
13094
13422
|
const triggerClass = usePrefixedClassNames('dateinput-trigger');
|
|
13423
|
+
// What the latest request asked for, so a second request in the same
|
|
13424
|
+
// event sees it before React renders. The callbacks fire here rather than
|
|
13425
|
+
// in a state updater, which StrictMode calls twice, and after the state
|
|
13426
|
+
// is set, so a request a callback makes is the one that lands last.
|
|
13427
|
+
const requestedOpenRef = React.useRef(false);
|
|
13095
13428
|
const setOpen = React.useCallback((next) => {
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
});
|
|
13429
|
+
if (requestedOpenRef.current === next)
|
|
13430
|
+
return;
|
|
13431
|
+
requestedOpenRef.current = next;
|
|
13432
|
+
setOpenState(next);
|
|
13433
|
+
if (next)
|
|
13434
|
+
onOpen?.();
|
|
13435
|
+
else
|
|
13436
|
+
onClose?.();
|
|
13105
13437
|
}, [onOpen, onClose]);
|
|
13106
13438
|
// Sync displayed text with value when value changes externally.
|
|
13107
13439
|
React.useEffect(() => {
|
|
@@ -13120,7 +13452,7 @@ const DateInputBase = React.forwardRef((props, ref) => {
|
|
|
13120
13452
|
}, [isControlled, onChange, isDayGranularity, granularity]);
|
|
13121
13453
|
// At month or year granularity the bounds widen to the periods holding
|
|
13122
13454
|
// them, so the month or year containing `min` stays reachable.
|
|
13123
|
-
const periodMin = React.useMemo(() =>
|
|
13455
|
+
const periodMin = React.useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
|
|
13124
13456
|
const periodMax = React.useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
|
|
13125
13457
|
const handleSelect = React.useCallback((d) => {
|
|
13126
13458
|
if (!isWithin(d, periodMin, periodMax))
|
|
@@ -13171,7 +13503,7 @@ const DateInputBase = React.forwardRef((props, ref) => {
|
|
|
13171
13503
|
]);
|
|
13172
13504
|
// Typing, and the re-parse on blur or Enter, that lands in the period the
|
|
13173
13505
|
// value already holds commits nothing, so a `value` elsewhere in its month
|
|
13174
|
-
// or year
|
|
13506
|
+
// or year changes only when the user picks or types a different period.
|
|
13175
13507
|
const commitTyped = React.useCallback((next) => {
|
|
13176
13508
|
if (next &&
|
|
13177
13509
|
value &&
|
|
@@ -13237,7 +13569,7 @@ const DateInputBase = React.forwardRef((props, ref) => {
|
|
|
13237
13569
|
? fromIsoValue(e.target.value, granularity)
|
|
13238
13570
|
: null;
|
|
13239
13571
|
commitValue(parsed);
|
|
13240
|
-
}, min: min ? toIsoValue(
|
|
13572
|
+
}, 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 }));
|
|
13241
13573
|
}
|
|
13242
13574
|
const chooseLabel = granularity === 'month'
|
|
13243
13575
|
? t.chooseMonth
|
|
@@ -13246,14 +13578,18 @@ const DateInputBase = React.forwardRef((props, ref) => {
|
|
|
13246
13578
|
: t.chooseDate;
|
|
13247
13579
|
const calendar = (jsxRuntime.jsx(Calendar
|
|
13248
13580
|
// A new granularity starts the calendar over on its own view.
|
|
13249
|
-
, { 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,
|
|
13581
|
+
, { 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,
|
|
13582
|
+
// The popover panel takes `popoverId`, so the calendar inside it
|
|
13583
|
+
// takes one of its own. Inline there is no panel, and the calendar
|
|
13584
|
+
// keeps the id it released with.
|
|
13585
|
+
id: inline ? popoverId : `${popoverId}-cal`, autoFocusCell: open, labels: labels }, granularity));
|
|
13250
13586
|
if (inline) {
|
|
13251
13587
|
return (jsxRuntime.jsxs("div", { ...cleanRest, ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), children: [calendar, name && (jsxRuntime.jsx("input", { type: "hidden", name: name, form: form, value: value ? toIsoValue(value, granularity) : '', required: required }))] }));
|
|
13252
13588
|
}
|
|
13253
13589
|
return (jsxRuntime.jsxs("div", { ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), ...cleanRest, children: [jsxRuntime.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 && (jsxRuntime.jsx("button", { type: "button", className: triggerClass, onClick: () => {
|
|
13254
13590
|
if (canOpen)
|
|
13255
13591
|
setOpen(!open);
|
|
13256
|
-
}, disabled: !canOpen, "aria-label": chooseLabel, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsxRuntime.jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsxRuntime.jsx(PickerPopover, { isOpen: open, onClose: handlePopoverClose, anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: chooseLabel, id: popoverId, restoreFocusRef: inputRef, children: calendar }))] }));
|
|
13592
|
+
}, disabled: !canOpen, "aria-label": chooseLabel, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsxRuntime.jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsxRuntime.jsx(PickerPopover, { isOpen: open, onClose: handlePopoverClose, anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: chooseLabel, id: popoverId, restoreFocusRef: inputRef, initialFocusSelector: CALENDAR_FOCUSED_CELL, children: calendar }))] }));
|
|
13257
13593
|
});
|
|
13258
13594
|
DateInputBase.displayName = 'DateInputBase';
|
|
13259
13595
|
|
|
@@ -13277,26 +13613,34 @@ DateInputBase.displayName = 'DateInputBase';
|
|
|
13277
13613
|
const DateInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
|
|
13278
13614
|
const insideField = useInsideField();
|
|
13279
13615
|
const insideControl = useInsideControl();
|
|
13616
|
+
const ownField = rendersOwnField('DateInput', {
|
|
13617
|
+
insideField,
|
|
13618
|
+
insideControl,
|
|
13619
|
+
label,
|
|
13620
|
+
message,
|
|
13621
|
+
horizontal,
|
|
13622
|
+
fieldClassName,
|
|
13623
|
+
});
|
|
13280
13624
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
13281
13625
|
label,
|
|
13282
13626
|
id: baseProps.id,
|
|
13283
13627
|
labelProps,
|
|
13284
13628
|
// Inline mode renders a bare calendar with no input to label.
|
|
13285
|
-
rendersLabel:
|
|
13629
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
13286
13630
|
});
|
|
13287
13631
|
const helpClass = usePrefixedClassNames('help', {
|
|
13288
13632
|
[`is-${messageColor}`]: !!messageColor,
|
|
13289
13633
|
});
|
|
13290
|
-
// The
|
|
13291
|
-
//
|
|
13292
|
-
let content = (jsxRuntime.jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps
|
|
13634
|
+
// The base hides its launcher while the Control it sits in is loading,
|
|
13635
|
+
// whether that is the one rendered below or an enclosing one.
|
|
13636
|
+
let content = (jsxRuntime.jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps }));
|
|
13293
13637
|
// Inline mode renders a bare calendar with no input, so the Control's
|
|
13294
13638
|
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
13295
13639
|
if (!insideControl && !baseProps.inline) {
|
|
13296
13640
|
content = (jsxRuntime.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 }));
|
|
13297
13641
|
}
|
|
13298
13642
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
13299
|
-
if (
|
|
13643
|
+
if (ownField) {
|
|
13300
13644
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
13301
13645
|
}
|
|
13302
13646
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
@@ -14170,7 +14514,10 @@ const fromIsoTime = (s) => {
|
|
|
14170
14514
|
* @returns {JSX.Element}
|
|
14171
14515
|
*/
|
|
14172
14516
|
const TimeInputBase = React.forwardRef((props, ref) => {
|
|
14173
|
-
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
|
|
14517
|
+
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;
|
|
14518
|
+
// The launcher gives way to the loading spinner of a Control this sits in.
|
|
14519
|
+
const controlLoading = useControlLoading();
|
|
14520
|
+
const triggerIcon = triggerIconProp ?? !controlLoading;
|
|
14174
14521
|
// Platform-appropriate feedback routing: when `haptics` is opted in and
|
|
14175
14522
|
// the runtime has no Vibration API (notably iOS Safari), enable the
|
|
14176
14523
|
// audio thunk so the user still gets *some* tactile cue. When vibrate
|
|
@@ -14226,23 +14573,29 @@ const TimeInputBase = React.forwardRef((props, ref) => {
|
|
|
14226
14573
|
const mobileFooterClass = usePrefixedClassNames('timeinput-footer-mobile');
|
|
14227
14574
|
const mobileFooterResetClass = usePrefixedClassNames('timeinput-footer-reset');
|
|
14228
14575
|
const mobileFooterDoneClass = usePrefixedClassNames('timeinput-footer-done');
|
|
14576
|
+
// What the latest request asked for, so a second request in the same
|
|
14577
|
+
// event sees it before React renders. The callbacks fire here rather than
|
|
14578
|
+
// in a state updater, which StrictMode calls twice, and after the state
|
|
14579
|
+
// is set, so a request a callback makes is the one that lands last.
|
|
14580
|
+
const requestedOpenRef = React.useRef(false);
|
|
14229
14581
|
const setOpen = React.useCallback((next) => {
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14241
|
-
});
|
|
14582
|
+
if (requestedOpenRef.current === next)
|
|
14583
|
+
return;
|
|
14584
|
+
requestedOpenRef.current = next;
|
|
14585
|
+
setOpenState(next);
|
|
14586
|
+
if (next) {
|
|
14587
|
+
valueAtOpenRef.current = value;
|
|
14588
|
+
onOpen?.();
|
|
14589
|
+
}
|
|
14590
|
+
else {
|
|
14591
|
+
onClose?.();
|
|
14592
|
+
}
|
|
14242
14593
|
}, [onOpen, onClose, value]);
|
|
14243
14594
|
React.useEffect(() => {
|
|
14244
14595
|
setText(value ? formatTime(value, defaultFormat, locale) : '');
|
|
14245
14596
|
}, [value, defaultFormat, locale]);
|
|
14597
|
+
// A value's date before year 1 is out of range, as in the date pickers.
|
|
14598
|
+
const lowerBound = React.useMemo(() => floorMin(min), [min]);
|
|
14246
14599
|
const commitValue = React.useCallback((next) => {
|
|
14247
14600
|
if (!isControlled)
|
|
14248
14601
|
setInternalValue(next);
|
|
@@ -14251,10 +14604,10 @@ const TimeInputBase = React.forwardRef((props, ref) => {
|
|
|
14251
14604
|
const handleSpinnerChange = React.useCallback((parts) => {
|
|
14252
14605
|
const base = value ?? new Date();
|
|
14253
14606
|
const next = setTimeOfDay(base, parts);
|
|
14254
|
-
if (!isWithin(next,
|
|
14607
|
+
if (!isWithin(next, lowerBound, max))
|
|
14255
14608
|
return;
|
|
14256
14609
|
commitValue(next);
|
|
14257
|
-
}, [value,
|
|
14610
|
+
}, [value, lowerBound, max, commitValue]);
|
|
14258
14611
|
const tryParse = React.useCallback((s) => {
|
|
14259
14612
|
const trimmed = s.trim();
|
|
14260
14613
|
if (!trimmed)
|
|
@@ -14286,7 +14639,7 @@ const TimeInputBase = React.forwardRef((props, ref) => {
|
|
|
14286
14639
|
setText,
|
|
14287
14640
|
makeBaseDate,
|
|
14288
14641
|
locale,
|
|
14289
|
-
min,
|
|
14642
|
+
min: lowerBound,
|
|
14290
14643
|
max,
|
|
14291
14644
|
isBlocked: unselectableTimes,
|
|
14292
14645
|
disabled,
|
|
@@ -14368,34 +14721,47 @@ TimeInputBase.displayName = 'TimeInputBase';
|
|
|
14368
14721
|
const TimeInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'clock', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
|
|
14369
14722
|
const insideField = useInsideField();
|
|
14370
14723
|
const insideControl = useInsideControl();
|
|
14724
|
+
const ownField = rendersOwnField('TimeInput', {
|
|
14725
|
+
insideField,
|
|
14726
|
+
insideControl,
|
|
14727
|
+
label,
|
|
14728
|
+
message,
|
|
14729
|
+
horizontal,
|
|
14730
|
+
fieldClassName,
|
|
14731
|
+
});
|
|
14371
14732
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
14372
14733
|
label,
|
|
14373
14734
|
id: baseProps.id,
|
|
14374
14735
|
labelProps,
|
|
14375
14736
|
// Inline mode renders a bare panel with no input to label.
|
|
14376
|
-
rendersLabel:
|
|
14737
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
14377
14738
|
});
|
|
14378
14739
|
const helpClass = usePrefixedClassNames('help', {
|
|
14379
14740
|
[`is-${messageColor}`]: !!messageColor,
|
|
14380
14741
|
});
|
|
14381
|
-
// The
|
|
14382
|
-
//
|
|
14383
|
-
let content = (jsxRuntime.jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps
|
|
14742
|
+
// The base hides its launcher while the Control it sits in is loading,
|
|
14743
|
+
// whether that is the one rendered below or an enclosing one.
|
|
14744
|
+
let content = (jsxRuntime.jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps }));
|
|
14384
14745
|
// Inline mode renders a bare panel with no input, so the Control's
|
|
14385
14746
|
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
14386
14747
|
if (!insideControl && !baseProps.inline) {
|
|
14387
14748
|
content = (jsxRuntime.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 }));
|
|
14388
14749
|
}
|
|
14389
14750
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
14390
|
-
if (
|
|
14751
|
+
if (ownField) {
|
|
14391
14752
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
14392
14753
|
}
|
|
14393
14754
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
14394
14755
|
});
|
|
14395
14756
|
TimeInput.displayName = 'TimeInput';
|
|
14396
14757
|
|
|
14758
|
+
// The year is padded to four digits, as `datetime-local` requires. HTML has
|
|
14759
|
+
// no such shape for a year before 1, so a date then is empty, as that input
|
|
14760
|
+
// would make it.
|
|
14397
14761
|
const toIsoDateTime = (d, withSeconds) => {
|
|
14398
|
-
|
|
14762
|
+
if (d.getFullYear() < FIRST_YEAR)
|
|
14763
|
+
return '';
|
|
14764
|
+
const yyyy = String(d.getFullYear()).padStart(4, '0');
|
|
14399
14765
|
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
|
14400
14766
|
const dd = String(d.getDate()).padStart(2, '0');
|
|
14401
14767
|
const hh = String(d.getHours()).padStart(2, '0');
|
|
@@ -14408,10 +14774,13 @@ const toIsoDateTime = (d, withSeconds) => {
|
|
|
14408
14774
|
const fromIsoDateTime = (s) => {
|
|
14409
14775
|
// The HTML datetime-local value may carry fractional seconds (the spec
|
|
14410
14776
|
// allows them and some engines normalize to `:ss.sss`); accept and drop.
|
|
14411
|
-
|
|
14777
|
+
// Its year is four or more digits, as `toIsoDateTime` writes one past 9999.
|
|
14778
|
+
const m = /^(\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?$/.exec(s);
|
|
14412
14779
|
if (!m)
|
|
14413
14780
|
return null;
|
|
14414
|
-
|
|
14781
|
+
const d = makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
14782
|
+
d.setHours(Number(m[4]), Number(m[5]), m[6] ? Number(m[6]) : 0, 0);
|
|
14783
|
+
return d;
|
|
14415
14784
|
};
|
|
14416
14785
|
/**
|
|
14417
14786
|
* Raw DateTimeInput — combined input + calendar + time spinner without
|
|
@@ -14424,7 +14793,10 @@ const fromIsoDateTime = (s) => {
|
|
|
14424
14793
|
const DateTimeInputBase = React.forwardRef((props, ref) => {
|
|
14425
14794
|
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,
|
|
14426
14795
|
// Step between visible wheel values; 1 gives the iOS-style every-minute wheel.
|
|
14427
|
-
incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon
|
|
14796
|
+
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;
|
|
14797
|
+
// The launcher gives way to the loading spinner of a Control this sits in.
|
|
14798
|
+
const controlLoading = useControlLoading();
|
|
14799
|
+
const triggerIcon = triggerIconProp ?? !controlLoading;
|
|
14428
14800
|
const t = mergeLabels(labels);
|
|
14429
14801
|
// Platform-appropriate feedback routing for the time wheel: on iOS (no
|
|
14430
14802
|
// navigator.vibrate) the audio thunk fills in; on Android the real haptic
|
|
@@ -14434,14 +14806,16 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
|
|
|
14434
14806
|
const isControlled = controlledValue !== undefined;
|
|
14435
14807
|
const [internalValue, setInternalValue] = React.useState(defaultValue ?? null);
|
|
14436
14808
|
const value = isControlled ? (controlledValue ?? null) : internalValue;
|
|
14437
|
-
|
|
14809
|
+
// Nothing before year 1 is in range, as in HTML's date inputs.
|
|
14810
|
+
const lowerBound = React.useMemo(() => floorMin(min), [min]);
|
|
14811
|
+
const initialFocused = React.useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
|
|
14438
14812
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14439
14813
|
[]);
|
|
14440
14814
|
const [focusedDate, setFocusedDate] = React.useState(initialFocused);
|
|
14441
14815
|
// Re-clamp focusedDate when min/max change so the focused cell stays valid.
|
|
14442
14816
|
React.useEffect(() => {
|
|
14443
|
-
setFocusedDate(prev => clampDate(prev,
|
|
14444
|
-
}, [
|
|
14817
|
+
setFocusedDate(prev => clampDate(prev, lowerBound, max));
|
|
14818
|
+
}, [lowerBound, max]);
|
|
14445
14819
|
const defaultFormat = format ??
|
|
14446
14820
|
(hourFormat === '12'
|
|
14447
14821
|
? enableSeconds
|
|
@@ -14498,19 +14872,23 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
|
|
|
14498
14872
|
const footerResetClass = usePrefixedClassNames('datetimeinput-footer-reset');
|
|
14499
14873
|
const footerDoneClass = usePrefixedClassNames('datetimeinput-footer-done');
|
|
14500
14874
|
const footerTimePillClass = usePrefixedClassNames('datetimeinput-footer-time-pill');
|
|
14875
|
+
// What the latest request asked for, so a second request in the same event
|
|
14876
|
+
// sees it before React renders. The callbacks fire here rather than in a
|
|
14877
|
+
// state updater, which StrictMode calls twice, and after the state is set,
|
|
14878
|
+
// so a request a callback makes is the one that lands last.
|
|
14879
|
+
const requestedOpenRef = React.useRef(false);
|
|
14501
14880
|
const setOpen = React.useCallback((next) => {
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
});
|
|
14881
|
+
if (requestedOpenRef.current === next)
|
|
14882
|
+
return;
|
|
14883
|
+
requestedOpenRef.current = next;
|
|
14884
|
+
setOpenState(next);
|
|
14885
|
+
if (next) {
|
|
14886
|
+
valueAtOpenRef.current = value;
|
|
14887
|
+
onOpen?.();
|
|
14888
|
+
}
|
|
14889
|
+
else {
|
|
14890
|
+
onClose?.();
|
|
14891
|
+
}
|
|
14514
14892
|
}, [onOpen, onClose, value]);
|
|
14515
14893
|
React.useEffect(() => {
|
|
14516
14894
|
setText(value ? formatDateTime(value, defaultFormat, locale) : '');
|
|
@@ -14534,18 +14912,18 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
|
|
|
14534
14912
|
minutes: value?.getMinutes() ?? 0,
|
|
14535
14913
|
seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
|
|
14536
14914
|
});
|
|
14537
|
-
if (!isWithin(merged,
|
|
14915
|
+
if (!isWithin(merged, lowerBound, max))
|
|
14538
14916
|
return;
|
|
14539
14917
|
commitValue(merged);
|
|
14540
14918
|
setFocusedDate(d);
|
|
14541
|
-
}, [value, enableSeconds,
|
|
14919
|
+
}, [value, enableSeconds, lowerBound, max, commitValue]);
|
|
14542
14920
|
const handleTimeChange = React.useCallback((parts) => {
|
|
14543
14921
|
const base = value ?? focusedDate;
|
|
14544
14922
|
const next = setTimeOfDay(base, parts);
|
|
14545
|
-
if (!isWithin(next,
|
|
14923
|
+
if (!isWithin(next, lowerBound, max))
|
|
14546
14924
|
return;
|
|
14547
14925
|
commitValue(next);
|
|
14548
|
-
}, [value, focusedDate,
|
|
14926
|
+
}, [value, focusedDate, lowerBound, max, commitValue]);
|
|
14549
14927
|
const tryParse = React.useCallback((s) => {
|
|
14550
14928
|
const trimmed = s.trim();
|
|
14551
14929
|
if (!trimmed)
|
|
@@ -14580,7 +14958,7 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
|
|
|
14580
14958
|
setText,
|
|
14581
14959
|
makeBaseDate,
|
|
14582
14960
|
locale,
|
|
14583
|
-
min,
|
|
14961
|
+
min: lowerBound,
|
|
14584
14962
|
max,
|
|
14585
14963
|
isBlocked,
|
|
14586
14964
|
disabled,
|
|
@@ -14613,7 +14991,7 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
|
|
|
14613
14991
|
? fromIsoDateTime(e.target.value)
|
|
14614
14992
|
: null;
|
|
14615
14993
|
commitValue(parsed);
|
|
14616
|
-
}, min: min ? toIsoDateTime(
|
|
14994
|
+
}, 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 }));
|
|
14617
14995
|
}
|
|
14618
14996
|
const spinnerValue = {
|
|
14619
14997
|
hours: value?.getHours() ?? 0,
|
|
@@ -14640,7 +15018,7 @@ const DateTimeInputBase = React.forwardRef((props, ref) => {
|
|
|
14640
15018
|
return (jsxRuntime.jsxs("div", { ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), ...cleanRest, children: [jsxRuntime.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 && (jsxRuntime.jsx("button", { type: "button", className: triggerClass, onClick: () => {
|
|
14641
15019
|
if (canOpen)
|
|
14642
15020
|
setOpen(!open);
|
|
14643
|
-
}, disabled: !canOpen, "aria-label": t.chooseDateTime, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsxRuntime.jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsxRuntime.jsx(PickerPopover, { isOpen: open, onClose: () => setOpen(false), anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: t.chooseDateTime, id: popoverId, restoreFocusRef: inputRef, children: panel }))] }));
|
|
15021
|
+
}, disabled: !canOpen, "aria-label": t.chooseDateTime, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsxRuntime.jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsxRuntime.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 }))] }));
|
|
14644
15022
|
});
|
|
14645
15023
|
DateTimeInputBase.displayName = 'DateTimeInputBase';
|
|
14646
15024
|
|
|
@@ -14664,26 +15042,34 @@ DateTimeInputBase.displayName = 'DateTimeInputBase';
|
|
|
14664
15042
|
const DateTimeInput = React.forwardRef(({ label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar-alt', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps }, ref) => {
|
|
14665
15043
|
const insideField = useInsideField();
|
|
14666
15044
|
const insideControl = useInsideControl();
|
|
15045
|
+
const ownField = rendersOwnField('DateTimeInput', {
|
|
15046
|
+
insideField,
|
|
15047
|
+
insideControl,
|
|
15048
|
+
label,
|
|
15049
|
+
message,
|
|
15050
|
+
horizontal,
|
|
15051
|
+
fieldClassName,
|
|
15052
|
+
});
|
|
14667
15053
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
14668
15054
|
label,
|
|
14669
15055
|
id: baseProps.id,
|
|
14670
15056
|
labelProps,
|
|
14671
15057
|
// Inline mode renders a bare picker with no input to label.
|
|
14672
|
-
rendersLabel:
|
|
15058
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
14673
15059
|
});
|
|
14674
15060
|
const helpClass = usePrefixedClassNames('help', {
|
|
14675
15061
|
[`is-${messageColor}`]: !!messageColor,
|
|
14676
15062
|
});
|
|
14677
|
-
// The
|
|
14678
|
-
//
|
|
14679
|
-
let content = (jsxRuntime.jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps
|
|
15063
|
+
// The base hides its launcher while the Control it sits in is loading,
|
|
15064
|
+
// whether that is the one rendered below or an enclosing one.
|
|
15065
|
+
let content = (jsxRuntime.jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps }));
|
|
14680
15066
|
// Inline mode renders a bare picker with no input, so the Control's
|
|
14681
15067
|
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
14682
15068
|
if (!insideControl && !baseProps.inline) {
|
|
14683
15069
|
content = (jsxRuntime.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 }));
|
|
14684
15070
|
}
|
|
14685
15071
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
14686
|
-
if (
|
|
15072
|
+
if (ownField) {
|
|
14687
15073
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
14688
15074
|
}
|
|
14689
15075
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
@@ -15622,31 +16008,57 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
|
|
|
15622
16008
|
// rewrites the shared element in place rather than removing and recreating
|
|
15623
16009
|
// it between the cleanup and the next run.
|
|
15624
16010
|
React.useEffect(() => () => setRootThemeRules(rootOrder, ''), [rootOrder]);
|
|
15625
|
-
//
|
|
15626
|
-
//
|
|
15627
|
-
// `
|
|
16011
|
+
// Bulma names its scheme attribute after the class prefix a stylesheet was
|
|
16012
|
+
// built with (`data-<prefix>theme`), so the prefixed builds read
|
|
16013
|
+
// `data-bestax-theme` and never `data-theme`.
|
|
16014
|
+
const classPrefix = useClassPrefix();
|
|
16015
|
+
// Toggle Bulma's light/dark scheme by writing the theme attribute on the
|
|
16016
|
+
// document root (<html>). This is always global, even on a scoped Theme.
|
|
16017
|
+
// Under a class prefix the prefixed attribute is written as well as
|
|
16018
|
+
// `data-theme`, so whichever build is loaded sees it; with no prefix only
|
|
16019
|
+
// `data-theme` is. `'system'` removes them so Bulma follows the OS
|
|
16020
|
+
// preference.
|
|
15628
16021
|
React.useEffect(() => {
|
|
15629
16022
|
if (colorMode === undefined) {
|
|
15630
16023
|
return;
|
|
15631
16024
|
}
|
|
15632
16025
|
const root = document.documentElement;
|
|
15633
|
-
const
|
|
15634
|
-
|
|
15635
|
-
|
|
15636
|
-
|
|
15637
|
-
|
|
15638
|
-
|
|
15639
|
-
|
|
15640
|
-
|
|
15641
|
-
return () => {
|
|
15642
|
-
if (previous === null) {
|
|
15643
|
-
root.removeAttribute('data-theme');
|
|
16026
|
+
const names = classPrefix
|
|
16027
|
+
? ['data-theme', `data-${classPrefix}theme`]
|
|
16028
|
+
: ['data-theme'];
|
|
16029
|
+
const previous = names.map(name => root.getAttribute(name));
|
|
16030
|
+
for (const name of names) {
|
|
16031
|
+
if (colorMode === 'system') {
|
|
16032
|
+
root.removeAttribute(name);
|
|
16033
|
+
continue;
|
|
15644
16034
|
}
|
|
15645
|
-
|
|
15646
|
-
root.setAttribute(
|
|
16035
|
+
try {
|
|
16036
|
+
root.setAttribute(name, colorMode);
|
|
15647
16037
|
}
|
|
16038
|
+
catch {
|
|
16039
|
+
// A class prefix can hold characters an attribute name cannot.
|
|
16040
|
+
// `data-theme` is still written, as it was before the prefixed
|
|
16041
|
+
// attribute existed, but a sheet built with that prefix keeps its
|
|
16042
|
+
// scheme, so say so.
|
|
16043
|
+
warnOnce('Theme:color-mode-prefix', `[bestax-bulma] <Theme colorMode="${colorMode}">: the class ` +
|
|
16044
|
+
`prefix "${classPrefix}" makes ${name} an invalid attribute ` +
|
|
16045
|
+
'name, so only data-theme is written, and a stylesheet built ' +
|
|
16046
|
+
'with that prefix keeps its scheme.');
|
|
16047
|
+
}
|
|
16048
|
+
}
|
|
16049
|
+
// Restore the previous values when colorMode changes or the component unmounts.
|
|
16050
|
+
return () => {
|
|
16051
|
+
names.forEach((name, i) => {
|
|
16052
|
+
const value = previous[i];
|
|
16053
|
+
if (value === null) {
|
|
16054
|
+
root.removeAttribute(name);
|
|
16055
|
+
}
|
|
16056
|
+
else {
|
|
16057
|
+
root.setAttribute(name, value);
|
|
16058
|
+
}
|
|
16059
|
+
});
|
|
15648
16060
|
};
|
|
15649
|
-
}, [colorMode]);
|
|
16061
|
+
}, [colorMode, classPrefix]);
|
|
15650
16062
|
// For local injection (when isRoot is false), prepare style object for CSS vars
|
|
15651
16063
|
const style = React.useMemo(() => {
|
|
15652
16064
|
if (isRoot) {
|