@allxsmith/bestax-bulma 5.26.3 → 5.26.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +607 -160
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +607 -160
- package/dist/index.esm.js.map +1 -1
- package/dist/types/form/Autocomplete.d.ts +6 -1
- package/dist/types/form/Checkboxes.d.ts +7 -0
- package/dist/types/form/DateInput.d.ts +22 -3
- package/dist/types/form/DateInputBase.d.ts +17 -3
- package/dist/types/form/DateTimeInput.d.ts +22 -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 +86 -7
- package/dist/types/form/Input.d.ts +8 -2
- package/dist/types/form/Numberinput.d.ts +3 -0
- 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 +8 -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 +8 -2
- package/dist/types/form/TimeInput.d.ts +22 -3
- package/dist/types/form/TimeInputBase.d.ts +16 -3
- package/dist/types/form/_pickerInternals/Calendar.d.ts +9 -3
- package/dist/types/form/_pickerInternals/dateUtils.d.ts +17 -0
- package/dist/types/form/controlLoading.d.ts +4 -0
- package/dist/types/helpers/useAnchoredPosition.d.ts +4 -1
- package/dist/types/helpers/useFocusTrap.d.ts +11 -4
- package/dist/types-cjs/form/Autocomplete.d.ts +6 -1
- package/dist/types-cjs/form/Checkboxes.d.ts +7 -0
- package/dist/types-cjs/form/DateInput.d.ts +22 -3
- package/dist/types-cjs/form/DateInputBase.d.ts +17 -3
- package/dist/types-cjs/form/DateTimeInput.d.ts +22 -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 +86 -7
- package/dist/types-cjs/form/Input.d.ts +8 -2
- package/dist/types-cjs/form/Numberinput.d.ts +3 -0
- 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 +8 -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 +8 -2
- package/dist/types-cjs/form/TimeInput.d.ts +22 -3
- package/dist/types-cjs/form/TimeInputBase.d.ts +16 -3
- package/dist/types-cjs/form/_pickerInternals/Calendar.d.ts +9 -3
- package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +17 -0
- package/dist/types-cjs/form/controlLoading.d.ts +4 -0
- package/dist/types-cjs/helpers/useAnchoredPosition.d.ts +4 -1
- package/dist/types-cjs/helpers/useFocusTrap.d.ts +11 -4
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/form/_datetimeinput.scss +9 -0
- package/src/scss/form/_picker-popover.scss +12 -9
- package/src/scss/form/_rate.scss +3 -1
- package/src/scss/form/_taginput.scss +8 -0
- package/src/scss/form/_timeinput.scss +5 -0
package/dist/index.esm.js
CHANGED
|
@@ -5199,10 +5199,17 @@ const focusedElement = (doc) => getDeepestActiveElement(doc) ?? doc.body;
|
|
|
5199
5199
|
* container, or give them a trap of their own. To trap content a `Portal`
|
|
5200
5200
|
* renders, pass a ref to the element inside the `Portal` as `containerRef`.
|
|
5201
5201
|
*
|
|
5202
|
-
* The
|
|
5203
|
-
*
|
|
5204
|
-
*
|
|
5205
|
-
*
|
|
5202
|
+
* The trap looks at `containerRef` again each time the component calling it
|
|
5203
|
+
* renders. A container that mounts after the trap turns on, such as a panel
|
|
5204
|
+
* waiting for a `Portal` target held in state, has it attach once that
|
|
5205
|
+
* component renders with the container in the DOM. When the container leaves
|
|
5206
|
+
* or is replaced, the trap lets go of it as it would turning off, handing
|
|
5207
|
+
* focus back, and attaches to the next one.
|
|
5208
|
+
* A child that mounts the container in a render of its own (from its own
|
|
5209
|
+
* state, or as a `Suspense` boundary resolves) goes unseen until the
|
|
5210
|
+
* component calling the hook renders again. The trap waits for hydration, so
|
|
5211
|
+
* that component renders again when the page hydrates, and portaled content,
|
|
5212
|
+
* which first appears then, is found.
|
|
5206
5213
|
*
|
|
5207
5214
|
* @function useFocusTrap
|
|
5208
5215
|
* @param containerRef - Ref to the element focus stays inside.
|
|
@@ -5222,12 +5229,18 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5222
5229
|
// Portaled content has no server-rendered counterpart, so a portaled
|
|
5223
5230
|
// container only exists from the commit after hydration.
|
|
5224
5231
|
const hydrated = useIsHydrated();
|
|
5232
|
+
// The container the trap is attached to, or `null` while it has none.
|
|
5233
|
+
const heldRef = useRef(null);
|
|
5234
|
+
// Bumped to run the trap again when the container in the ref changes.
|
|
5235
|
+
const [refChanges, setRefChanges] = useState(0);
|
|
5225
5236
|
useEffect(() => {
|
|
5237
|
+
heldRef.current = null;
|
|
5226
5238
|
if (!active || !hydrated)
|
|
5227
5239
|
return undefined;
|
|
5228
5240
|
const container = containerRef.current;
|
|
5229
5241
|
if (!container)
|
|
5230
5242
|
return undefined;
|
|
5243
|
+
heldRef.current = container;
|
|
5231
5244
|
const doc = container.ownerDocument;
|
|
5232
5245
|
const opener = focusedElement(doc);
|
|
5233
5246
|
// `initialFocusRef` counts only when focusing it puts focus inside. Focus
|
|
@@ -5302,7 +5315,28 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
5302
5315
|
target.focus();
|
|
5303
5316
|
}
|
|
5304
5317
|
};
|
|
5305
|
-
}, [
|
|
5318
|
+
}, [
|
|
5319
|
+
active,
|
|
5320
|
+
hydrated,
|
|
5321
|
+
containerRef,
|
|
5322
|
+
initialFocusRef,
|
|
5323
|
+
restoreFocus,
|
|
5324
|
+
refChanges,
|
|
5325
|
+
]);
|
|
5326
|
+
// Nothing says when a ref is set, so after every render, a ref that no
|
|
5327
|
+
// longer matches what the trap holds (a container that mounted, left or was
|
|
5328
|
+
// replaced) runs it again. Effects run in order, so whenever the effect
|
|
5329
|
+
// above ran in this commit, it read the ref just before this does, and a
|
|
5330
|
+
// mismatch means the ref changed in a render it didn't run for. The render
|
|
5331
|
+
// a bump asks for runs it, so the two match and the trap settles. An empty
|
|
5332
|
+
// ref matches a trap holding nothing, so a container that never mounts
|
|
5333
|
+
// costs no render.
|
|
5334
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
|
|
5335
|
+
useEffect(() => {
|
|
5336
|
+
if (active && hydrated && containerRef.current !== heldRef.current) {
|
|
5337
|
+
setRefChanges(n => n + 1);
|
|
5338
|
+
}
|
|
5339
|
+
});
|
|
5306
5340
|
}
|
|
5307
5341
|
|
|
5308
5342
|
// INTERNAL: deliberately not exported from src/index.ts.
|
|
@@ -8735,6 +8769,108 @@ const useInsideControl = () => useContext(ControlContext);
|
|
|
8735
8769
|
const FieldProvider = FieldContext.Provider;
|
|
8736
8770
|
/** Provider for Control context — used internally by Control component. */
|
|
8737
8771
|
const ControlProvider = ControlContext.Provider;
|
|
8772
|
+
// Each Field-shaping prop, with what takes its place on a `Field` the caller
|
|
8773
|
+
// wraps around the `Control`. `message` needs nothing there, because the
|
|
8774
|
+
// wrapper still renders it inside `Field > Control`. The other Field-level
|
|
8775
|
+
// props (`labelSize`, `labelProps`, `messageColor`) change nothing unless a
|
|
8776
|
+
// `label` or `message` renders, so on their own they keep no `Field`.
|
|
8777
|
+
// `bare-control.test.tsx` holds this list to what each wrapper hands its
|
|
8778
|
+
// `Field` and to which props change its markup, so a new Field-level prop
|
|
8779
|
+
// fails there until it is sorted into one group or the other.
|
|
8780
|
+
const FIELD_SHAPING = [
|
|
8781
|
+
['label', 'label'],
|
|
8782
|
+
['message', undefined],
|
|
8783
|
+
['horizontal', 'horizontal'],
|
|
8784
|
+
['fieldClassName', 'className (for fieldClassName)'],
|
|
8785
|
+
];
|
|
8786
|
+
/** `a`, `a and b`, `a, b and c`. */
|
|
8787
|
+
const listOf = (items) => items.length > 1
|
|
8788
|
+
? `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`
|
|
8789
|
+
: items[0];
|
|
8790
|
+
/**
|
|
8791
|
+
* Whether a convenience form wrapper renders a `Field` of its own (#905).
|
|
8792
|
+
* Never inside an outer `Field`. Inside a `Control` with no `Field` around
|
|
8793
|
+
* it, a `Field` of its own would put a `.field` inside the `.control`, where
|
|
8794
|
+
* Bulma's control styles break (its icon rules are sibling selectors on the
|
|
8795
|
+
* input). So it renders one there only when a prop that shapes the `Field`'s
|
|
8796
|
+
* markup is set (`label`, `message`, `horizontal`, `fieldClassName`), keeping
|
|
8797
|
+
* the markup it always had, and warns in development that the fix is a
|
|
8798
|
+
* `Field` around that `Control`. A prop counts when it is truthy, the same
|
|
8799
|
+
* test that decides whether it changes the `Field`'s output.
|
|
8800
|
+
* Internal; not part of the public API.
|
|
8801
|
+
*/
|
|
8802
|
+
const rendersOwnField = (component, options) => {
|
|
8803
|
+
if (options.insideField)
|
|
8804
|
+
return false;
|
|
8805
|
+
if (!options.insideControl)
|
|
8806
|
+
return true;
|
|
8807
|
+
const held = FIELD_SHAPING.filter(([prop]) => options[prop]);
|
|
8808
|
+
if (held.length === 0)
|
|
8809
|
+
return false;
|
|
8810
|
+
const names = held.map(([prop]) => prop);
|
|
8811
|
+
const moved = held
|
|
8812
|
+
.map(([, onField]) => onField)
|
|
8813
|
+
.filter((onField) => onField !== undefined);
|
|
8814
|
+
warnOnce(`${component}:Field-in-bare-Control:${names.join('+')}`, `[bestax-bulma] <${component} ${names.join(' ')}> inside a <Control> ` +
|
|
8815
|
+
`with no <Field> around it renders its own <Field> for ` +
|
|
8816
|
+
`${names.length > 1 ? 'those props' : 'that prop'}, which puts a ` +
|
|
8817
|
+
`.field inside the .control, a nesting Bulma's styles are not ` +
|
|
8818
|
+
`written for. Wrap the <Control> in a <Field> instead` +
|
|
8819
|
+
(moved.length > 0 ? `, and set ${listOf(moved)} on that <Field>.` : '.'));
|
|
8820
|
+
return true;
|
|
8821
|
+
};
|
|
8822
|
+
// Each Control-level prop, with what takes its place on the `Control` the
|
|
8823
|
+
// caller wrapped around the wrapper. `bare-control.test.tsx` holds this to
|
|
8824
|
+
// what each wrapper hands its own `Control`, so a new Control-level prop
|
|
8825
|
+
// fails there until it is added here and passed to `rendersOwnControl`.
|
|
8826
|
+
const ON_CONTROL = {
|
|
8827
|
+
isLoading: 'isLoading',
|
|
8828
|
+
iconLeft: 'iconLeft',
|
|
8829
|
+
iconLeftName: 'iconLeftName',
|
|
8830
|
+
iconLeftSize: 'iconLeftSize',
|
|
8831
|
+
iconRight: 'iconRight',
|
|
8832
|
+
iconRightName: 'iconRightName',
|
|
8833
|
+
iconRightSize: 'iconRightSize',
|
|
8834
|
+
hasIconsLeft: 'hasIconsLeft',
|
|
8835
|
+
hasIconsRight: 'hasIconsRight',
|
|
8836
|
+
isExpanded: 'isExpanded',
|
|
8837
|
+
controlSize: 'size (for controlSize)',
|
|
8838
|
+
controlClassName: 'className (for controlClassName)',
|
|
8839
|
+
};
|
|
8840
|
+
/**
|
|
8841
|
+
* Whether a convenience form wrapper renders a `Control` of its own (#921):
|
|
8842
|
+
* only outside one. Inside a `Control` it renders none, so the props it
|
|
8843
|
+
* would have handed its own do nothing there, and it warns in development,
|
|
8844
|
+
* naming them and saying to set them on that `Control`. An `inline` picker
|
|
8845
|
+
* renders none in either place, so it drops them inside a `Control` or not,
|
|
8846
|
+
* and its warning says so without pointing at a `Control`. A wrapper passes
|
|
8847
|
+
* only the props it hands its own `Control`, as the caller gave them, so a
|
|
8848
|
+
* default the wrapper fills in itself never warns. A prop counts when it is
|
|
8849
|
+
* truthy, since a falsy one would change nothing on a `Control` either.
|
|
8850
|
+
* Internal; not part of the public API.
|
|
8851
|
+
*/
|
|
8852
|
+
const rendersOwnControl = (component, options) => {
|
|
8853
|
+
if (!options.inline && !options.insideControl)
|
|
8854
|
+
return true;
|
|
8855
|
+
const held = Object.keys(ON_CONTROL).filter(prop => options[prop]);
|
|
8856
|
+
if (held.length === 0)
|
|
8857
|
+
return false;
|
|
8858
|
+
const names = held.join(' ');
|
|
8859
|
+
const doNothing = held.length > 1 ? 'those props do' : 'that prop does';
|
|
8860
|
+
if (options.inline) {
|
|
8861
|
+
warnOnce(`${component}:Control-props-inline:${held.join('+')}`, `[bestax-bulma] <${component} inline ${names}> renders no <Control> ` +
|
|
8862
|
+
`in inline mode, inside a <Control> or not, so ${doNothing} ` +
|
|
8863
|
+
`nothing. Leave ${held.length > 1 ? 'them' : 'it'} out of an ` +
|
|
8864
|
+
`inline picker.`);
|
|
8865
|
+
}
|
|
8866
|
+
else {
|
|
8867
|
+
warnOnce(`${component}:Control-props-in-Control:${held.join('+')}`, `[bestax-bulma] <${component} ${names}> inside a <Control> renders ` +
|
|
8868
|
+
`no <Control> of its own, so ${doNothing} nothing there. Set ` +
|
|
8869
|
+
`${listOf(held.map(prop => ON_CONTROL[prop]))} on that <Control> ` +
|
|
8870
|
+
`instead.`);
|
|
8871
|
+
}
|
|
8872
|
+
return false;
|
|
8873
|
+
};
|
|
8738
8874
|
const FieldLabelIdContext = createContext(undefined);
|
|
8739
8875
|
/**
|
|
8740
8876
|
* The id a labeled Field wants its single composed control to adopt (#495).
|
|
@@ -8865,6 +9001,19 @@ const Checkbox = forwardRef(({ color, size, className, children, textColor, disa
|
|
|
8865
9001
|
});
|
|
8866
9002
|
Checkbox.displayName = 'Checkbox';
|
|
8867
9003
|
|
|
9004
|
+
// Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
|
|
9005
|
+
// picker bases read it so their right-side launcher gives way to that
|
|
9006
|
+
// spinner, which sits at the same edge, whether the `Control` is the one a
|
|
9007
|
+
// convenience input renders or one the caller wrapped around the input.
|
|
9008
|
+
// Autocomplete reads it for its clear button, which sits there too.
|
|
9009
|
+
// Kept apart from `FormContext`, whose contexts only report whether a `Field`
|
|
9010
|
+
// or `Control` is present. Internal; not part of the public API.
|
|
9011
|
+
const ControlLoadingContext = createContext(false);
|
|
9012
|
+
/** Provider for the loading state, used internally by `Control`. */
|
|
9013
|
+
const ControlLoadingProvider = ControlLoadingContext.Provider;
|
|
9014
|
+
/** Whether the nearest enclosing `Control` is loading; `false` outside one. */
|
|
9015
|
+
const useControlLoading = () => useContext(ControlLoadingContext);
|
|
9016
|
+
|
|
8868
9017
|
const allowedColors = [...validColors, 'inherit', 'current'];
|
|
8869
9018
|
/**
|
|
8870
9019
|
* 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 +9073,7 @@ const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isL
|
|
|
8924
9073
|
});
|
|
8925
9074
|
const controlClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
8926
9075
|
// --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
|
|
8927
|
-
return (jsx(ControlProvider, { value: true, children: jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [children, leftIconValue &&
|
|
9076
|
+
return (jsx(ControlProvider, { value: true, children: jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
|
|
8928
9077
|
(isIconProps(leftIconValue) ? (jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
|
|
8929
9078
|
(isIconProps(rightIconValue) ? (jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
|
|
8930
9079
|
});
|
|
@@ -9149,10 +9298,18 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, }) {
|
|
|
9149
9298
|
const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
|
|
9150
9299
|
const insideField = useInsideField();
|
|
9151
9300
|
const insideControl = useInsideControl();
|
|
9301
|
+
const ownField = rendersOwnField('Checkboxes', {
|
|
9302
|
+
insideField,
|
|
9303
|
+
insideControl,
|
|
9304
|
+
label,
|
|
9305
|
+
message,
|
|
9306
|
+
horizontal,
|
|
9307
|
+
fieldClassName,
|
|
9308
|
+
});
|
|
9152
9309
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
9153
9310
|
label,
|
|
9154
9311
|
labelProps,
|
|
9155
|
-
rendersLabel:
|
|
9312
|
+
rendersLabel: ownField,
|
|
9156
9313
|
});
|
|
9157
9314
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
9158
9315
|
...props,
|
|
@@ -9178,10 +9335,10 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
|
|
|
9178
9335
|
}), [name, groupActive, currentValue, handleChange]);
|
|
9179
9336
|
const checkboxesElement = (jsx("div", { className: wrapperClass, role: "group", "aria-labelledby": ariaLabelledBy, ...rest, children: jsx(CheckboxesProvider, { value: ctx, children: children }) }));
|
|
9180
9337
|
let content = checkboxesElement;
|
|
9181
|
-
if (
|
|
9338
|
+
if (rendersOwnControl('Checkboxes', { insideControl })) {
|
|
9182
9339
|
content = jsx(Control, { children: content });
|
|
9183
9340
|
}
|
|
9184
|
-
if (
|
|
9341
|
+
if (ownField) {
|
|
9185
9342
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9186
9343
|
}
|
|
9187
9344
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -9200,11 +9357,20 @@ const File = forwardRef(({
|
|
|
9200
9357
|
// Field props
|
|
9201
9358
|
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
|
|
9202
9359
|
const insideField = useInsideField();
|
|
9360
|
+
const insideControl = useInsideControl();
|
|
9361
|
+
const ownField = rendersOwnField('File', {
|
|
9362
|
+
insideField,
|
|
9363
|
+
insideControl,
|
|
9364
|
+
label,
|
|
9365
|
+
message,
|
|
9366
|
+
horizontal,
|
|
9367
|
+
fieldClassName,
|
|
9368
|
+
});
|
|
9203
9369
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
9204
9370
|
label,
|
|
9205
9371
|
id: props.id,
|
|
9206
9372
|
labelProps,
|
|
9207
|
-
rendersLabel:
|
|
9373
|
+
rendersLabel: ownField,
|
|
9208
9374
|
});
|
|
9209
9375
|
const { classPrefix } = useConfig();
|
|
9210
9376
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
@@ -9236,7 +9402,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
9236
9402
|
});
|
|
9237
9403
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
9238
9404
|
const fileElement = (jsx("div", { className: fileClass, children: jsxs("label", { className: usePrefixedClassNames('file-label'), children: [jsx("input", { ref: ref, className: classNames(usePrefixedClassNames('file-input'), inputClassName), type: "file", id: controlId, ...rest }), jsxs("span", { className: usePrefixedClassNames('file-cta'), children: [iconLeft && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconLeft })), jsx("span", { className: usePrefixedClassNames('file-label'), children: buttonLabel || 'Choose a file\u2026' }), iconRight && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-icon'), children: iconRight }))] }), hasName && fileName && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: fileName }))] }) }));
|
|
9239
|
-
if (
|
|
9405
|
+
if (ownField) {
|
|
9240
9406
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
|
|
9241
9407
|
}
|
|
9242
9408
|
return (jsxs(Fragment, { children: [fileElement, messageEl] }));
|
|
@@ -9369,10 +9535,18 @@ Radio.displayName = 'Radio';
|
|
|
9369
9535
|
const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
|
|
9370
9536
|
const insideField = useInsideField();
|
|
9371
9537
|
const insideControl = useInsideControl();
|
|
9538
|
+
const ownField = rendersOwnField('Radios', {
|
|
9539
|
+
insideField,
|
|
9540
|
+
insideControl,
|
|
9541
|
+
label,
|
|
9542
|
+
message,
|
|
9543
|
+
horizontal,
|
|
9544
|
+
fieldClassName,
|
|
9545
|
+
});
|
|
9372
9546
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
9373
9547
|
label,
|
|
9374
9548
|
labelProps,
|
|
9375
|
-
rendersLabel:
|
|
9549
|
+
rendersLabel: ownField,
|
|
9376
9550
|
});
|
|
9377
9551
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
9378
9552
|
...props,
|
|
@@ -9401,10 +9575,10 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
|
|
|
9401
9575
|
}), [name, groupActive, currentValue, handleChange]);
|
|
9402
9576
|
const radiosElement = (jsx("div", { className: wrapperClass, role: "radiogroup", "aria-labelledby": ariaLabelledBy, ...rest, children: jsx(RadiosProvider, { value: ctx, children: children }) }));
|
|
9403
9577
|
let content = radiosElement;
|
|
9404
|
-
if (
|
|
9578
|
+
if (rendersOwnControl('Radios', { insideControl })) {
|
|
9405
9579
|
content = jsx(Control, { children: content });
|
|
9406
9580
|
}
|
|
9407
|
-
if (
|
|
9581
|
+
if (ownField) {
|
|
9408
9582
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9409
9583
|
}
|
|
9410
9584
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -9584,6 +9758,14 @@ const Slider = forwardRef((props, ref) => {
|
|
|
9584
9758
|
range: _range, value: _value, defaultValue: _defaultValue, onChange: _onChange, ariaLabel: _ariaLabel, minDistance: _minDistance, nameLow: _nameLow, nameHigh: _nameHigh, ...restProps } = props;
|
|
9585
9759
|
const insideField = useInsideField();
|
|
9586
9760
|
const insideControl = useInsideControl();
|
|
9761
|
+
const ownField = rendersOwnField('Slider', {
|
|
9762
|
+
insideField,
|
|
9763
|
+
insideControl,
|
|
9764
|
+
label,
|
|
9765
|
+
message,
|
|
9766
|
+
horizontal,
|
|
9767
|
+
fieldClassName,
|
|
9768
|
+
});
|
|
9587
9769
|
const { bulmaHelperClasses, rest } = useBulmaClasses(restProps);
|
|
9588
9770
|
// In range mode the low thumb carries `rest` (and any user id), so the
|
|
9589
9771
|
// label targets it; each thumb keeps its own aria-label for AT.
|
|
@@ -9591,7 +9773,7 @@ const Slider = forwardRef((props, ref) => {
|
|
|
9591
9773
|
label,
|
|
9592
9774
|
id: restProps.id,
|
|
9593
9775
|
labelProps,
|
|
9594
|
-
rendersLabel:
|
|
9776
|
+
rendersLabel: ownField,
|
|
9595
9777
|
});
|
|
9596
9778
|
// Resolve tooltip mode: explicit tooltip prop takes precedence, else showOutput maps to 'auto'
|
|
9597
9779
|
const tooltipMode = tooltip ?? (showOutput ? 'auto' : 'hidden');
|
|
@@ -9927,10 +10109,10 @@ const Slider = forwardRef((props, ref) => {
|
|
|
9927
10109
|
'--slider-output-arrow-offset': `calc(50% - ${nudgeXHigh}px)`,
|
|
9928
10110
|
}, children: formatValue(currentRange[1]) })), renderTicks()] }));
|
|
9929
10111
|
let content = sliderElement;
|
|
9930
|
-
if (
|
|
10112
|
+
if (rendersOwnControl('Slider', { insideControl })) {
|
|
9931
10113
|
content = jsx(Control, { children: content });
|
|
9932
10114
|
}
|
|
9933
|
-
if (
|
|
10115
|
+
if (ownField) {
|
|
9934
10116
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
9935
10117
|
}
|
|
9936
10118
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -10160,10 +10342,18 @@ function getFillPercent(iconIndex, value) {
|
|
|
10160
10342
|
const Rate = forwardRef(({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = 0, max = 5, size, disabled = false, showScore = false, showText = false, texts, onChange, customIcon, spaced = false, rtl = false, iconName, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, precision = 1, customText, name, form, className, ...props }, ref) => {
|
|
10161
10343
|
const insideField = useInsideField();
|
|
10162
10344
|
const insideControl = useInsideControl();
|
|
10345
|
+
const ownField = rendersOwnField('Rate', {
|
|
10346
|
+
insideField,
|
|
10347
|
+
insideControl,
|
|
10348
|
+
label,
|
|
10349
|
+
message,
|
|
10350
|
+
horizontal,
|
|
10351
|
+
fieldClassName,
|
|
10352
|
+
});
|
|
10163
10353
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
10164
10354
|
label,
|
|
10165
10355
|
labelProps,
|
|
10166
|
-
rendersLabel:
|
|
10356
|
+
rendersLabel: ownField,
|
|
10167
10357
|
});
|
|
10168
10358
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
10169
10359
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
@@ -10363,10 +10553,10 @@ const Rate = forwardRef(({ label, labelSize, labelProps, horizontal, message, me
|
|
|
10363
10553
|
const rateCustomTextClass = usePrefixedClassNames('rate-custom-text');
|
|
10364
10554
|
const rateElement = (jsxs("div", { ref: ref, className: combinedClasses, role: "radiogroup", "aria-label": ariaLabelledBy ? undefined : 'Rating', "aria-labelledby": ariaLabelledBy, "aria-valuenow": currentValue, "aria-valuemin": 0, "aria-valuemax": max, "aria-valuetext": ariaValueText, tabIndex: disabled ? -1 : 0, onKeyDown: handleKeyDown, ...rest, children: [jsx("div", { className: rateItemsClass, children: renderIcons() }), showScore && (jsx("span", { className: rateScoreClass, children: getScoreDisplay() })), text && jsx("span", { className: rateTextClass, children: text }), customText && (jsx("span", { className: rateCustomTextClass, children: customText })), name && (jsx("input", { type: "hidden", name: name, value: currentValue, form: form }))] }));
|
|
10365
10555
|
let content = rateElement;
|
|
10366
|
-
if (
|
|
10556
|
+
if (rendersOwnControl('Rate', { insideControl })) {
|
|
10367
10557
|
content = jsx(Control, { children: content });
|
|
10368
10558
|
}
|
|
10369
|
-
if (
|
|
10559
|
+
if (ownField) {
|
|
10370
10560
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
10371
10561
|
}
|
|
10372
10562
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -10602,7 +10792,11 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
10602
10792
|
[`is-${messageColor}`]: !!messageColor,
|
|
10603
10793
|
});
|
|
10604
10794
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
10605
|
-
|
|
10795
|
+
// The clear button gives way to a spinner at its spot: the `loading` one,
|
|
10796
|
+
// or that of a loading Control this sits in.
|
|
10797
|
+
const controlLoading = useControlLoading();
|
|
10798
|
+
const showClear = clearable && !!inputValue && !disabled && !loading && !controlLoading;
|
|
10799
|
+
const autocompleteElement = (jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxs("div", { className: controlClasses, children: [jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: placeholder, disabled: disabled, name: name, form: form, required: required, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": isActive, "aria-haspopup": "listbox", "aria-autocomplete": "list", autoComplete: "off" }), showClear && (jsx("button", { type: "button", className: iconRightClickableClass, onClick: handleClear, "aria-label": "Clear", children: jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "16", height: "16", "aria-hidden": "true", children: [jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsx("span", { className: iconRightClass, children: jsx("span", { className: loaderClass }) }))] }), isActive && (filteredData.length > 0 || empty) && (jsx("div", { className: dropdownMenuClasses, children: jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, role: "listbox", onScroll: handleDropdownScroll, children: [header && jsx("div", { className: dropdownHeaderClass, children: header }), filteredData.length > 0 ? (filteredData.map((item, index) => {
|
|
10606
10800
|
const isDisabled = typeof item !== 'string' && item.disabled;
|
|
10607
10801
|
const isHighlighted = index === highlightedIndex;
|
|
10608
10802
|
const itemClasses = prefixedClassNames(classPrefix, 'dropdown-item', {
|
|
@@ -10647,6 +10841,15 @@ const Taginput = forwardRef(({
|
|
|
10647
10841
|
// Field props
|
|
10648
10842
|
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
10843
|
const insideField = useInsideField();
|
|
10844
|
+
const insideControl = useInsideControl();
|
|
10845
|
+
const ownField = rendersOwnField('Taginput', {
|
|
10846
|
+
insideField,
|
|
10847
|
+
insideControl,
|
|
10848
|
+
label,
|
|
10849
|
+
message,
|
|
10850
|
+
horizontal,
|
|
10851
|
+
fieldClassName,
|
|
10852
|
+
});
|
|
10650
10853
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
10651
10854
|
const { classPrefix } = useConfig();
|
|
10652
10855
|
const defaultIconLibrary = useIconLibrary();
|
|
@@ -10668,7 +10871,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
10668
10871
|
label,
|
|
10669
10872
|
id,
|
|
10670
10873
|
labelProps,
|
|
10671
|
-
rendersLabel:
|
|
10874
|
+
rendersLabel: ownField && !isMaxReached,
|
|
10672
10875
|
});
|
|
10673
10876
|
// Get display value from tag
|
|
10674
10877
|
const getDisplayValue = (tag) => {
|
|
@@ -10940,7 +11143,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
10940
11143
|
'is-active': index === highlightedIndex,
|
|
10941
11144
|
}), onClick: () => addTag(item), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": index === highlightedIndex, children: item }, index))) }) })), showCounter && jsx("small", { className: counterClasses, children: counterText }), name &&
|
|
10942
11145
|
tags.map((tag, i) => (jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
|
|
10943
|
-
if (
|
|
11146
|
+
if (ownField) {
|
|
10944
11147
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
|
|
10945
11148
|
}
|
|
10946
11149
|
return (jsxs(Fragment, { children: [taginputElement, messageEl] }));
|
|
@@ -11093,21 +11296,44 @@ fieldClassName, controlClassName,
|
|
|
11093
11296
|
...inputProps }, ref) => {
|
|
11094
11297
|
const insideField = useInsideField();
|
|
11095
11298
|
const insideControl = useInsideControl();
|
|
11299
|
+
const ownField = rendersOwnField('Input', {
|
|
11300
|
+
insideField,
|
|
11301
|
+
insideControl,
|
|
11302
|
+
label,
|
|
11303
|
+
message,
|
|
11304
|
+
horizontal,
|
|
11305
|
+
fieldClassName,
|
|
11306
|
+
});
|
|
11307
|
+
const ownControl = rendersOwnControl('Input', {
|
|
11308
|
+
insideControl,
|
|
11309
|
+
isLoading,
|
|
11310
|
+
iconLeft,
|
|
11311
|
+
iconLeftName,
|
|
11312
|
+
iconLeftSize,
|
|
11313
|
+
iconRight,
|
|
11314
|
+
iconRightName,
|
|
11315
|
+
iconRightSize,
|
|
11316
|
+
hasIconsLeft,
|
|
11317
|
+
hasIconsRight,
|
|
11318
|
+
isExpanded,
|
|
11319
|
+
controlSize,
|
|
11320
|
+
controlClassName,
|
|
11321
|
+
});
|
|
11096
11322
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11097
11323
|
label,
|
|
11098
11324
|
id: inputProps.id,
|
|
11099
11325
|
labelProps,
|
|
11100
|
-
rendersLabel:
|
|
11326
|
+
rendersLabel: ownField,
|
|
11101
11327
|
});
|
|
11102
11328
|
const helpClass = usePrefixedClassNames('help', {
|
|
11103
11329
|
[`is-${messageColor}`]: !!messageColor,
|
|
11104
11330
|
});
|
|
11105
11331
|
let content = jsx(InputBase, { ref: ref, id: controlId, ...inputProps });
|
|
11106
|
-
if (
|
|
11332
|
+
if (ownControl) {
|
|
11107
11333
|
content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
11108
11334
|
}
|
|
11109
11335
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
11110
|
-
if (
|
|
11336
|
+
if (ownField) {
|
|
11111
11337
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11112
11338
|
}
|
|
11113
11339
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -11143,21 +11369,40 @@ fieldClassName, controlClassName,
|
|
|
11143
11369
|
...selectProps }, ref) => {
|
|
11144
11370
|
const insideField = useInsideField();
|
|
11145
11371
|
const insideControl = useInsideControl();
|
|
11372
|
+
const ownField = rendersOwnField('Select', {
|
|
11373
|
+
insideField,
|
|
11374
|
+
insideControl,
|
|
11375
|
+
label,
|
|
11376
|
+
message,
|
|
11377
|
+
horizontal,
|
|
11378
|
+
fieldClassName,
|
|
11379
|
+
});
|
|
11380
|
+
// Not isLoading, which the select draws itself and so keeps in a Control.
|
|
11381
|
+
const ownControl = rendersOwnControl('Select', {
|
|
11382
|
+
insideControl,
|
|
11383
|
+
iconLeft,
|
|
11384
|
+
iconLeftName,
|
|
11385
|
+
iconLeftSize,
|
|
11386
|
+
hasIconsLeft,
|
|
11387
|
+
isExpanded,
|
|
11388
|
+
controlSize,
|
|
11389
|
+
controlClassName,
|
|
11390
|
+
});
|
|
11146
11391
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11147
11392
|
label,
|
|
11148
11393
|
id: selectProps.id,
|
|
11149
11394
|
labelProps,
|
|
11150
|
-
rendersLabel:
|
|
11395
|
+
rendersLabel: ownField,
|
|
11151
11396
|
});
|
|
11152
11397
|
const helpClass = usePrefixedClassNames('help', {
|
|
11153
11398
|
[`is-${messageColor}`]: !!messageColor,
|
|
11154
11399
|
});
|
|
11155
11400
|
let content = jsx(SelectBase, { ref: ref, id: controlId, ...selectProps });
|
|
11156
|
-
if (
|
|
11401
|
+
if (ownControl) {
|
|
11157
11402
|
content = (jsx(Control, { iconLeft: iconLeft, iconLeftName: iconLeftName, iconLeftSize: iconLeftSize, hasIconsLeft: hasIconsLeft, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
11158
11403
|
}
|
|
11159
11404
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
11160
|
-
if (
|
|
11405
|
+
if (ownField) {
|
|
11161
11406
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11162
11407
|
}
|
|
11163
11408
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -11194,21 +11439,35 @@ fieldClassName, controlClassName,
|
|
|
11194
11439
|
...textAreaProps }, ref) => {
|
|
11195
11440
|
const insideField = useInsideField();
|
|
11196
11441
|
const insideControl = useInsideControl();
|
|
11442
|
+
const ownField = rendersOwnField('TextArea', {
|
|
11443
|
+
insideField,
|
|
11444
|
+
insideControl,
|
|
11445
|
+
label,
|
|
11446
|
+
message,
|
|
11447
|
+
horizontal,
|
|
11448
|
+
fieldClassName,
|
|
11449
|
+
});
|
|
11450
|
+
const ownControl = rendersOwnControl('TextArea', {
|
|
11451
|
+
insideControl,
|
|
11452
|
+
isLoading: controlIsLoading,
|
|
11453
|
+
controlSize,
|
|
11454
|
+
controlClassName,
|
|
11455
|
+
});
|
|
11197
11456
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
11198
11457
|
label,
|
|
11199
11458
|
id: textAreaProps.id,
|
|
11200
11459
|
labelProps,
|
|
11201
|
-
rendersLabel:
|
|
11460
|
+
rendersLabel: ownField,
|
|
11202
11461
|
});
|
|
11203
11462
|
const helpClass = usePrefixedClassNames('help', {
|
|
11204
11463
|
[`is-${messageColor}`]: !!messageColor,
|
|
11205
11464
|
});
|
|
11206
11465
|
let content = jsx(TextAreaBase, { ref: ref, id: controlId, ...textAreaProps });
|
|
11207
|
-
if (
|
|
11466
|
+
if (ownControl) {
|
|
11208
11467
|
content = (jsx(Control, { isLoading: controlIsLoading, size: controlSize, className: controlClassName, children: content }));
|
|
11209
11468
|
}
|
|
11210
11469
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
11211
|
-
if (
|
|
11470
|
+
if (ownField) {
|
|
11212
11471
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
11213
11472
|
}
|
|
11214
11473
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -11295,6 +11554,21 @@ function makeDate(year, month = 0, day = 1) {
|
|
|
11295
11554
|
d.setFullYear(year, month, day);
|
|
11296
11555
|
return d;
|
|
11297
11556
|
}
|
|
11557
|
+
/**
|
|
11558
|
+
* The earliest year the pickers accept. HTML's date, month and datetime-local
|
|
11559
|
+
* inputs hold no year below 1, so the calendar, typing and parsing stop there
|
|
11560
|
+
* too.
|
|
11561
|
+
*/
|
|
11562
|
+
const FIRST_YEAR = 1;
|
|
11563
|
+
/**
|
|
11564
|
+
* The lower bound a picker applies: `min`, raised to local midnight on
|
|
11565
|
+
* 1 January of {@link FIRST_YEAR} when it is earlier or absent. A `min` in
|
|
11566
|
+
* range comes back as the same object.
|
|
11567
|
+
*/
|
|
11568
|
+
function floorMin(min) {
|
|
11569
|
+
const floor = makeDate(FIRST_YEAR);
|
|
11570
|
+
return min && min.getTime() >= floor.getTime() ? min : floor;
|
|
11571
|
+
}
|
|
11298
11572
|
/**
|
|
11299
11573
|
* First instant of the day, month or year containing `d`. Works by setters on
|
|
11300
11574
|
* a copy rather than the `Date(y, m, d)` constructor, which reads years 0–99
|
|
@@ -11388,12 +11662,14 @@ function setTimeOfDay(d, parts) {
|
|
|
11388
11662
|
r.setSeconds(parts.seconds);
|
|
11389
11663
|
return r;
|
|
11390
11664
|
}
|
|
11665
|
+
/**
|
|
11666
|
+
* A copy of `d` moved into `[min, max]`. When `min` is after `max` nothing is
|
|
11667
|
+
* in range and `min` wins, so clamping twice gives the same date and a
|
|
11668
|
+
* picker's focus stays off the years before 1 under a `max` before then.
|
|
11669
|
+
*/
|
|
11391
11670
|
function clampDate(d, min, max) {
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
if (max && isAfter(d, max))
|
|
11395
|
-
return new Date(max);
|
|
11396
|
-
return new Date(d);
|
|
11671
|
+
const capped = max && isAfter(d, max) ? max : d;
|
|
11672
|
+
return new Date(min && isBefore(capped, min) ? min : capped);
|
|
11397
11673
|
}
|
|
11398
11674
|
/**
|
|
11399
11675
|
* Snap a Date's time-of-day to the nearest grid defined by the given
|
|
@@ -11466,7 +11742,9 @@ function formatToken(d, token) {
|
|
|
11466
11742
|
case 'YYYY':
|
|
11467
11743
|
// Zero-pad to a fixed 4 chars so the rendered width matches the
|
|
11468
11744
|
// segment-map width during digit-by-digit keyboard entry (and to match
|
|
11469
|
-
// ISO 8601)
|
|
11745
|
+
// ISO 8601), so year 19 shows as `0019`. Years >9999 keep their natural
|
|
11746
|
+
// width. The pickers commit no year below 1, so a minus sign reaches
|
|
11747
|
+
// this only from a `value` passed in.
|
|
11470
11748
|
return pad(d.getFullYear(), 4);
|
|
11471
11749
|
case 'YY':
|
|
11472
11750
|
return pad(d.getFullYear() % 100);
|
|
@@ -11740,9 +12018,10 @@ function getMonthNames(locale, length = 'long') {
|
|
|
11740
12018
|
|
|
11741
12019
|
/**
|
|
11742
12020
|
* Selects the calendar's focused cell, the one tab stop of whichever grid or
|
|
11743
|
-
* year list is on show, for a popover to put focus on as it opens.
|
|
12021
|
+
* year list is on show, for a popover to put focus on as it opens. It runs
|
|
12022
|
+
* against the whole panel, so it matches only inside the grid or the list.
|
|
11744
12023
|
*/
|
|
11745
|
-
const CALENDAR_FOCUSED_CELL = '[data-focused="true"], [data-focused-year="true"]';
|
|
12024
|
+
const CALENDAR_FOCUSED_CELL = '[role="grid"] [data-focused="true"], [role="listbox"] [data-focused-year="true"]';
|
|
11746
12025
|
const BASE_VIEW = {
|
|
11747
12026
|
day: 'days',
|
|
11748
12027
|
month: 'months',
|
|
@@ -11816,10 +12095,11 @@ function nearestEnabled(i, disabled) {
|
|
|
11816
12095
|
return i;
|
|
11817
12096
|
}
|
|
11818
12097
|
/**
|
|
11819
|
-
* The year list's keyboard
|
|
11820
|
-
* year or a row, Home/End go to the list's ends. Returns the
|
|
11821
|
-
* from and the direction to search past disabled years, or
|
|
11822
|
-
* the list doesn't handle.
|
|
12098
|
+
* The year list's keyboard, as the selection surface and as navigation:
|
|
12099
|
+
* arrows move a year or a row, Home/End go to the list's ends. Returns the
|
|
12100
|
+
* year to start from and the direction to search past disabled years, or
|
|
12101
|
+
* `null` for a key the list doesn't handle. As navigation no year is
|
|
12102
|
+
* disabled, so only the year is used.
|
|
11823
12103
|
*/
|
|
11824
12104
|
function yearStep(key, focusedYear, years) {
|
|
11825
12105
|
switch (key) {
|
|
@@ -11839,7 +12119,7 @@ function yearStep(key, focusedYear, years) {
|
|
|
11839
12119
|
return null;
|
|
11840
12120
|
}
|
|
11841
12121
|
}
|
|
11842
|
-
const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', }) => {
|
|
12122
|
+
const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minProp, max, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, locale, dayNames, monthNames, nearbyMonthDays = true, color, size, className, id, autoFocusCell = false, labels, yearsRange, granularity = 'day', }) => {
|
|
11843
12123
|
const gridRef = useRef(null);
|
|
11844
12124
|
const monthGridRef = useRef(null);
|
|
11845
12125
|
const yearGridRef = useRef(null);
|
|
@@ -11849,6 +12129,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11849
12129
|
const isDayGranularity = granularity === 'day';
|
|
11850
12130
|
const isYearGranularity = granularity === 'year';
|
|
11851
12131
|
const [view, setView] = useState(baseView);
|
|
12132
|
+
// Nothing before year 1 can be picked or reached, as in HTML's date inputs.
|
|
12133
|
+
const min = useMemo(() => floorMin(minProp), [minProp]);
|
|
11852
12134
|
const computedDayNames = useMemo(() => {
|
|
11853
12135
|
if (dayNames && dayNames.length === 7)
|
|
11854
12136
|
return dayNames;
|
|
@@ -11919,7 +12201,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11919
12201
|
const moveFocus = useCallback((next, direction) => {
|
|
11920
12202
|
let candidate = next;
|
|
11921
12203
|
for (let i = 0; i < 366; i++) {
|
|
11922
|
-
if (
|
|
12204
|
+
if (candidate.getTime() < startOfDay(min).getTime())
|
|
11923
12205
|
return;
|
|
11924
12206
|
if (max && candidate.getTime() > startOfDay(max).getTime())
|
|
11925
12207
|
return;
|
|
@@ -11985,7 +12267,7 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
11985
12267
|
let candidate = next;
|
|
11986
12268
|
for (let i = 0; i < 120; i++) {
|
|
11987
12269
|
const month = startOfMonth(candidate).getTime();
|
|
11988
|
-
if (
|
|
12270
|
+
if (month < startOfMonth(min).getTime())
|
|
11989
12271
|
return;
|
|
11990
12272
|
if (max && month > startOfMonth(max).getTime())
|
|
11991
12273
|
return;
|
|
@@ -12033,25 +12315,22 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12033
12315
|
const today = new Date();
|
|
12034
12316
|
const todayYear = today.getFullYear();
|
|
12035
12317
|
// The header steps a month at a time over the day grid and a year at a
|
|
12036
|
-
// time over the month grid, kept inside min/max.
|
|
12037
|
-
|
|
12038
|
-
|
|
12318
|
+
// time over the month grid, kept inside min/max. A step is off once the
|
|
12319
|
+
// month or year on show holds the bound or lies past it, as picking a year
|
|
12320
|
+
// from the list can leave it.
|
|
12039
12321
|
const prevAnchor = isDayGranularity
|
|
12040
|
-
?
|
|
12322
|
+
? addMonths(focusedDate, -1)
|
|
12041
12323
|
: clampDate(addYears(focusedDate, -1), min, max);
|
|
12042
12324
|
const nextAnchor = isDayGranularity
|
|
12043
|
-
?
|
|
12325
|
+
? addMonths(focusedDate, 1)
|
|
12044
12326
|
: clampDate(addYears(focusedDate, 1), min, max);
|
|
12327
|
+
const focusedMonth = startOfMonth(focusedDate).getTime();
|
|
12045
12328
|
const prevDisabled = isDayGranularity
|
|
12046
|
-
?
|
|
12047
|
-
|
|
12048
|
-
isSameMonth(focusedDate, min))
|
|
12049
|
-
: !!(min && focusedYear <= min.getFullYear());
|
|
12329
|
+
? focusedMonth <= startOfMonth(min).getTime()
|
|
12330
|
+
: focusedYear <= min.getFullYear();
|
|
12050
12331
|
const nextDisabled = isDayGranularity
|
|
12051
|
-
? !!
|
|
12052
|
-
|
|
12053
|
-
isSameMonth(focusedDate, max))
|
|
12054
|
-
: !!(max && focusedYear >= max.getFullYear());
|
|
12332
|
+
? !!max && focusedMonth >= startOfMonth(max).getTime()
|
|
12333
|
+
: !!max && focusedYear >= max.getFullYear();
|
|
12055
12334
|
const labelId = id ? `${id}-label` : undefined;
|
|
12056
12335
|
const monthLabel = isDayGranularity
|
|
12057
12336
|
? `${computedMonthNames[focusedDate.getMonth()]} ${focusedYear}`
|
|
@@ -12062,7 +12341,9 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12062
12341
|
const [mountYear] = useState(focusedYear);
|
|
12063
12342
|
const windowYear = isYearGranularity ? mountYear : focusedYear;
|
|
12064
12343
|
const yearList = useMemo(() => {
|
|
12065
|
-
const minYear =
|
|
12344
|
+
const minYear = minProp
|
|
12345
|
+
? min.getFullYear()
|
|
12346
|
+
: Math.max(FIRST_YEAR, windowYear - 100);
|
|
12066
12347
|
const maxYear = max ? max.getFullYear() : windowYear + 100;
|
|
12067
12348
|
const [lo, hi] = yearsRange ?? [minYear, maxYear];
|
|
12068
12349
|
const start = Math.max(lo, minYear);
|
|
@@ -12071,20 +12352,31 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12071
12352
|
for (let y = start; y <= end; y++)
|
|
12072
12353
|
out.push(y);
|
|
12073
12354
|
return out;
|
|
12074
|
-
}, [windowYear, min, max, yearsRange]);
|
|
12355
|
+
}, [windowYear, minProp, min, max, yearsRange]);
|
|
12075
12356
|
// Only the selection surface disables years; as navigation every listed
|
|
12076
12357
|
// year can be visited.
|
|
12077
12358
|
const disabledYears = useMemo(() => new Set(isYearGranularity ? yearList.filter(y => isYearUnselectable(y)) : []), [isYearGranularity, yearList, isYearUnselectable]);
|
|
12078
12359
|
// The year list's one tab stop, after the month grid's: when the focused
|
|
12079
12360
|
// year is disabled the stop moves to the nearest enabled year in the list,
|
|
12080
|
-
// and focusing it makes it the focused year.
|
|
12081
|
-
// the
|
|
12361
|
+
// and focusing it makes it the focused year. As the selection surface a
|
|
12362
|
+
// year outside a non-empty list starts from the end it fell past, so the
|
|
12363
|
+
// list keeps a stop. As navigation it marks the current year, so a year
|
|
12364
|
+
// outside the list marks no option, and the stop itself follows focus.
|
|
12082
12365
|
const tabStopYear = useMemo(() => {
|
|
12083
|
-
|
|
12084
|
-
if (i < 0)
|
|
12085
|
-
|
|
12366
|
+
let i = yearList.indexOf(focusedYear);
|
|
12367
|
+
if (i < 0) {
|
|
12368
|
+
if (!isYearGranularity || yearList.length === 0)
|
|
12369
|
+
return focusedYear;
|
|
12370
|
+
i = focusedYear < yearList[0] ? 0 : yearList.length - 1;
|
|
12371
|
+
}
|
|
12086
12372
|
return yearList[nearestEnabled(i, yearList.map(y => disabledYears.has(y)))];
|
|
12087
|
-
}, [yearList, focusedYear, disabledYears]);
|
|
12373
|
+
}, [yearList, focusedYear, disabledYears, isYearGranularity]);
|
|
12374
|
+
// As navigation the keys move focus and leave the focused year alone, so
|
|
12375
|
+
// the tab stop goes to the year that last had focus while it is listed,
|
|
12376
|
+
// and Tab leaves the list rather than landing on the current year. It
|
|
12377
|
+
// starts over each time the list opens.
|
|
12378
|
+
const [navYear, setNavYear] = useState(null);
|
|
12379
|
+
const listTabStop = navYear !== null && yearList.includes(navYear) ? navYear : tabStopYear;
|
|
12088
12380
|
// Which of the focused year's months have no selectable day.
|
|
12089
12381
|
const disabledMonths = useMemo(() => granularity === 'month'
|
|
12090
12382
|
? Array.from({ length: 12 }, (_, m) => isMonthUnselectable(makeDate(focusedYear, m)))
|
|
@@ -12120,19 +12412,39 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12120
12412
|
}
|
|
12121
12413
|
}, [yearList, disabledYears, onFocusedDateChange, min, max]);
|
|
12122
12414
|
const handleYearListKeyDown = useCallback((e) => {
|
|
12123
|
-
if (
|
|
12124
|
-
|
|
12125
|
-
if (
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12415
|
+
if (isYearGranularity) {
|
|
12416
|
+
const step = yearStep(e.key, focusedYear, yearList);
|
|
12417
|
+
if (!step)
|
|
12418
|
+
return;
|
|
12419
|
+
e.preventDefault();
|
|
12420
|
+
moveYearFocus(step[0], step[1]);
|
|
12421
|
+
return;
|
|
12422
|
+
}
|
|
12423
|
+
// As navigation, Escape returns to the grid the list was opened from.
|
|
12424
|
+
// It stops there, so a popover around the calendar stays open and only
|
|
12425
|
+
// a second Escape closes it.
|
|
12426
|
+
if (e.key === 'Escape') {
|
|
12427
|
+
e.preventDefault();
|
|
12428
|
+
e.stopPropagation();
|
|
12429
|
+
setView(baseView);
|
|
12129
12430
|
return;
|
|
12130
12431
|
}
|
|
12131
|
-
|
|
12432
|
+
// The other keys move DOM focus from the year that has it and leave
|
|
12433
|
+
// the focused date alone. The list is centred on that date, so moving
|
|
12434
|
+
// it would reflow the list under the keyboard, and it would turn the
|
|
12435
|
+
// grid behind the list before anything is picked. Enter or Space
|
|
12436
|
+
// clicks the year with focus, which picks it.
|
|
12437
|
+
const options = Array.from(e.currentTarget.querySelectorAll('[role="option"]'));
|
|
12438
|
+
const from = options.indexOf(e.target);
|
|
12439
|
+
if (from < 0)
|
|
12440
|
+
return;
|
|
12441
|
+
const step = yearStep(e.key, yearList[from], yearList);
|
|
12132
12442
|
if (!step)
|
|
12133
12443
|
return;
|
|
12134
12444
|
e.preventDefault();
|
|
12135
|
-
|
|
12445
|
+
// The listed years run one apart, so a year's index is its distance
|
|
12446
|
+
// from the first. A step past either end finds no option.
|
|
12447
|
+
options[step[0] - yearList[0]]?.focus();
|
|
12136
12448
|
}, [isYearGranularity, baseView, focusedYear, yearList, moveYearFocus]);
|
|
12137
12449
|
// The day or month grid takes focus as the popover opens on it, or comes
|
|
12138
12450
|
// back to it from the year list.
|
|
@@ -12203,7 +12515,10 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12203
12515
|
list.querySelector('[data-focused-year="true"]')?.focus();
|
|
12204
12516
|
}, [isYearGranularity, focusedYear]);
|
|
12205
12517
|
const navArrow = (d) => (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", children: jsx("path", { d: d, fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
12206
|
-
return (jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () =>
|
|
12518
|
+
return (jsxs("div", { className: classNames(calendarClass, className), id: id, children: [jsxs("div", { className: headerClass, children: [isYearGranularity ? (jsx("span", { className: monthLabelClass, id: labelId, children: monthLabel })) : (jsxs("button", { type: "button", className: monthTriggerClass, "aria-haspopup": "listbox", "aria-expanded": view === 'years', onClick: () => {
|
|
12519
|
+
setNavYear(null);
|
|
12520
|
+
setView(v => (v === 'years' ? baseView : 'years'));
|
|
12521
|
+
}, children: [jsx("span", { className: monthLabelClass, id: labelId, "aria-live": "polite", children: monthLabel }), jsx("svg", { className: monthCaretClass, viewBox: "0 0 12 12", width: "10", height: "10", "aria-hidden": "true", children: jsx("path", { d: "M2 4l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) })] })), !isYearGranularity && (jsxs("div", { className: navGroupClass, children: [jsx("button", { type: "button", className: navPrevClass, "aria-label": isDayGranularity ? t.prevMonth : t.prevYear, disabled: prevDisabled || view !== baseView, onClick: () => onFocusedDateChange(prevAnchor), children: navArrow('M10 3l-5 5 5 5') }), jsx("button", { type: "button", className: navNextClass, "aria-label": isDayGranularity ? t.nextMonth : t.nextYear, disabled: nextDisabled || view !== baseView, onClick: () => onFocusedDateChange(nextAnchor), children: navArrow('M6 3l5 5-5 5') })] }))] }), view === 'days' && (jsxs(Fragment, { children: [jsx("div", { className: dayNamesRowClass, "aria-hidden": "true", children: computedDayNames.map((name, i) => (jsx("div", { className: dayNameClass, children: name }, i))) }), jsx("div", { ref: gridRef, role: "grid", "aria-labelledby": labelId, className: gridClass, onKeyDown: handleKeyDown, ...gridFocusHandlers, children: cells.map(cell => {
|
|
12207
12522
|
const disabled = isDateUnselectable(cell.date);
|
|
12208
12523
|
const isSelected = !!value && isSameDay(value, cell.date);
|
|
12209
12524
|
const isFocused = isSameDay(cell.date, tabStopDay);
|
|
@@ -12263,10 +12578,16 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min, max,
|
|
|
12263
12578
|
'is-today': isToday,
|
|
12264
12579
|
'is-disabled': disabled,
|
|
12265
12580
|
});
|
|
12266
|
-
return (jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex:
|
|
12267
|
-
//
|
|
12268
|
-
//
|
|
12269
|
-
|
|
12581
|
+
return (jsx("button", { type: "button", role: "option", "aria-selected": isSelected, "aria-disabled": isYearGranularity ? disabled : undefined, "aria-current": isYearGranularity && isToday ? 'date' : undefined, "data-focused-year": isFocused ? 'true' : undefined, tabIndex: year === listTabStop ? 0 : -1, disabled: disabled, className: cellCls, onFocus: () => {
|
|
12582
|
+
// As navigation, focusing a year by key or pointer only
|
|
12583
|
+
// moves the tab stop: nothing is committed until a year is
|
|
12584
|
+
// picked. As the selection surface it is reached by Tab
|
|
12585
|
+
// while the focused year is disabled, or by pointer, and
|
|
12586
|
+
// keys move on from here.
|
|
12587
|
+
if (!isYearGranularity) {
|
|
12588
|
+
setNavYear(year);
|
|
12589
|
+
}
|
|
12590
|
+
else if (year !== focusedYear) {
|
|
12270
12591
|
onFocusedDateChange(clampDate(makeDate(year), min, max));
|
|
12271
12592
|
}
|
|
12272
12593
|
}, onClick: () => isYearGranularity
|
|
@@ -12291,11 +12612,10 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
|
|
|
12291
12612
|
active: isOpen,
|
|
12292
12613
|
position,
|
|
12293
12614
|
fixed: appendToBody,
|
|
12294
|
-
//
|
|
12295
|
-
//
|
|
12296
|
-
// the panel.
|
|
12297
|
-
|
|
12298
|
-
offset: appendToBody ? 0 : undefined,
|
|
12615
|
+
// The gap comes from `--bulma-picker-popover-offset` as a margin, in place
|
|
12616
|
+
// or portaled, so the coordinates leave it out and `auto` reads it off
|
|
12617
|
+
// the panel.
|
|
12618
|
+
offset: 0,
|
|
12299
12619
|
styledGap: marginGap,
|
|
12300
12620
|
});
|
|
12301
12621
|
useEffect(() => {
|
|
@@ -12741,33 +13061,35 @@ function useSegmentedEntry(params) {
|
|
|
12741
13061
|
// within the same event tick.
|
|
12742
13062
|
const typedDigitsRef = useRef('');
|
|
12743
13063
|
// The text focus seeded an empty picker with, until the user edits it or
|
|
12744
|
-
// leaves.
|
|
13064
|
+
// leaves.
|
|
12745
13065
|
const seedRef = useRef(null);
|
|
12746
|
-
// Whether the
|
|
12747
|
-
//
|
|
12748
|
-
|
|
13066
|
+
// Whether the text is still what the picker put there, so nothing in it was
|
|
13067
|
+
// typed: the value's display, or an empty picker's seed until an edit drops
|
|
13068
|
+
// it. Against a value it compares text alone, so typing that ends back on
|
|
13069
|
+
// the display counts as untouched.
|
|
13070
|
+
const isUntouched = useCallback((s) => value
|
|
13071
|
+
? s === formatFn(value, format, locale)
|
|
13072
|
+
: seedRef.current !== null && s === seedRef.current, [value, formatFn, format, locale]);
|
|
12749
13073
|
// A seed the popover never turned into a value closes with it, wherever
|
|
12750
13074
|
// focus goes, so a dismissed empty field stays empty.
|
|
12751
13075
|
//
|
|
12752
13076
|
// As the popover closes, its focus trap hands focus back to the input. That
|
|
12753
13077
|
// focus is not the user arriving: it must not open the popover again under
|
|
12754
|
-
// `openOnFocus`, or seed an empty picker
|
|
12755
|
-
//
|
|
12756
|
-
//
|
|
12757
|
-
//
|
|
12758
|
-
// in between is the one handed back.
|
|
13078
|
+
// `openOnFocus`, or seed an empty picker. `closingRef` is set in the layout
|
|
13079
|
+
// phase of the commit that closes it and cleared by that commit's effects,
|
|
13080
|
+
// and React runs a commit's effect cleanups, the trap's among them, before
|
|
13081
|
+
// any of its effects, so a focus in between is the one handed back.
|
|
12759
13082
|
const closingRef = useRef(false);
|
|
12760
13083
|
const wasOpenRef = useRef(isOpen);
|
|
12761
13084
|
useLayoutEffect(() => {
|
|
12762
13085
|
if (wasOpenRef.current && !isOpen) {
|
|
12763
13086
|
closingRef.current = true;
|
|
12764
|
-
if (!value &&
|
|
13087
|
+
if (!value && isUntouched(text))
|
|
12765
13088
|
setText('');
|
|
12766
|
-
}
|
|
12767
13089
|
seedRef.current = null;
|
|
12768
13090
|
}
|
|
12769
13091
|
wasOpenRef.current = isOpen;
|
|
12770
|
-
}, [isOpen, value, text, setText]);
|
|
13092
|
+
}, [isOpen, value, isUntouched, text, setText]);
|
|
12771
13093
|
useEffect(() => {
|
|
12772
13094
|
closingRef.current = false;
|
|
12773
13095
|
}, [isOpen]);
|
|
@@ -12845,13 +13167,18 @@ function useSegmentedEntry(params) {
|
|
|
12845
13167
|
}
|
|
12846
13168
|
setActiveSegmentIdx(null);
|
|
12847
13169
|
typedDigitsRef.current = '';
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
//
|
|
12853
|
-
//
|
|
12854
|
-
|
|
13170
|
+
// Leaving commits only what was typed. Re-parsing the value's display
|
|
13171
|
+
// could drop what the format leaves out, such as seconds, and a seed
|
|
13172
|
+
// nobody typed over is not the user's date.
|
|
13173
|
+
const untouched = isUntouched(text);
|
|
13174
|
+
// An open popover keeps an unused seed until it closes, so one portaled
|
|
13175
|
+
// outside the picker, whose focus reads as leaving, keeps it as one
|
|
13176
|
+
// inside does.
|
|
13177
|
+
if (!isOpen)
|
|
13178
|
+
seedRef.current = null;
|
|
13179
|
+
if (untouched) {
|
|
13180
|
+
if (!value && !isOpen)
|
|
13181
|
+
setText('');
|
|
12855
13182
|
onBlur?.(e);
|
|
12856
13183
|
return;
|
|
12857
13184
|
}
|
|
@@ -12865,6 +13192,8 @@ function useSegmentedEntry(params) {
|
|
|
12865
13192
|
onBlur?.(e);
|
|
12866
13193
|
}, [
|
|
12867
13194
|
containerRef,
|
|
13195
|
+
isUntouched,
|
|
13196
|
+
isOpen,
|
|
12868
13197
|
tryParse,
|
|
12869
13198
|
text,
|
|
12870
13199
|
isAllowed,
|
|
@@ -12878,7 +13207,6 @@ function useSegmentedEntry(params) {
|
|
|
12878
13207
|
]);
|
|
12879
13208
|
const handleFocus = useCallback((e) => {
|
|
12880
13209
|
const handedBack = closingRef.current;
|
|
12881
|
-
handedBackRef.current = handedBack;
|
|
12882
13210
|
if (!handedBack && openOnFocus && popover && !disabled && !readOnly) {
|
|
12883
13211
|
setOpen(true);
|
|
12884
13212
|
}
|
|
@@ -13046,12 +13374,20 @@ function useSegmentedEntry(params) {
|
|
|
13046
13374
|
}
|
|
13047
13375
|
if (e.key === 'Enter') {
|
|
13048
13376
|
e.preventDefault();
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13377
|
+
// Untouched text has nothing to commit, as on leaving, but Enter
|
|
13378
|
+
// still confirms what the field shows.
|
|
13379
|
+
if (isUntouched(text)) {
|
|
13052
13380
|
if (closeOnSelect)
|
|
13053
13381
|
setOpen(false);
|
|
13054
13382
|
}
|
|
13383
|
+
else {
|
|
13384
|
+
const parsed = tryParse(text);
|
|
13385
|
+
if (parsed && isAllowed(parsed)) {
|
|
13386
|
+
commitValue(parsed);
|
|
13387
|
+
if (closeOnSelect)
|
|
13388
|
+
setOpen(false);
|
|
13389
|
+
}
|
|
13390
|
+
}
|
|
13055
13391
|
}
|
|
13056
13392
|
onKeyDown?.(e);
|
|
13057
13393
|
}, [
|
|
@@ -13065,6 +13401,7 @@ function useSegmentedEntry(params) {
|
|
|
13065
13401
|
setOpen,
|
|
13066
13402
|
closeOnSelect,
|
|
13067
13403
|
popover,
|
|
13404
|
+
isUntouched,
|
|
13068
13405
|
tryParse,
|
|
13069
13406
|
text,
|
|
13070
13407
|
isAllowed,
|
|
@@ -13113,9 +13450,12 @@ const pad2$1 = (n) => String(n).padStart(2, '0');
|
|
|
13113
13450
|
* The value as the native input and the hidden form input carry it:
|
|
13114
13451
|
* `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
|
|
13115
13452
|
* `<input type="month">` use. The year is padded to four digits, as those
|
|
13116
|
-
* inputs require and as the `YYYY` token displays it.
|
|
13453
|
+
* inputs require and as the `YYYY` token displays it. HTML has no such shape
|
|
13454
|
+
* for a year before 1, so a date then is empty, as those inputs would make it.
|
|
13117
13455
|
*/
|
|
13118
13456
|
const toIsoValue = (d, granularity) => {
|
|
13457
|
+
if (d.getFullYear() < FIRST_YEAR)
|
|
13458
|
+
return '';
|
|
13119
13459
|
const year = String(d.getFullYear()).padStart(4, '0');
|
|
13120
13460
|
if (granularity === 'year')
|
|
13121
13461
|
return year;
|
|
@@ -13156,14 +13496,19 @@ const SKIPPED_SEGMENTS = {
|
|
|
13156
13496
|
* @returns {JSX.Element}
|
|
13157
13497
|
*/
|
|
13158
13498
|
const DateInputBase = forwardRef((props, ref) => {
|
|
13159
|
-
const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, granularity = 'day', format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = true, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon
|
|
13499
|
+
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;
|
|
13500
|
+
// The launcher gives way to the loading spinner of a Control this sits in.
|
|
13501
|
+
const controlLoading = useControlLoading();
|
|
13502
|
+
const triggerIcon = triggerIconProp ?? !controlLoading;
|
|
13160
13503
|
const t = mergeLabels(labels);
|
|
13161
13504
|
const isDayGranularity = granularity === 'day';
|
|
13162
13505
|
const resolvedFormat = format ?? DEFAULT_FORMATS[granularity];
|
|
13163
13506
|
const isControlled = controlledValue !== undefined;
|
|
13164
13507
|
const [internalValue, setInternalValue] = useState(defaultValue ?? null);
|
|
13165
13508
|
const value = isControlled ? (controlledValue ?? null) : internalValue;
|
|
13166
|
-
|
|
13509
|
+
// Nothing before year 1 is in range, as in HTML's date inputs.
|
|
13510
|
+
const lowerBound = useMemo(() => floorMin(min), [min]);
|
|
13511
|
+
const initialFocused = useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
|
|
13167
13512
|
// intentionally only on mount: keep focusedDate stable until value/open change
|
|
13168
13513
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
13169
13514
|
[]);
|
|
@@ -13171,8 +13516,8 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13171
13516
|
// Re-clamp focusedDate if min/max change after mount so the focused cell
|
|
13172
13517
|
// never disappears outside the displayable range.
|
|
13173
13518
|
useEffect(() => {
|
|
13174
|
-
setFocusedDate(prev => clampDate(prev,
|
|
13175
|
-
}, [
|
|
13519
|
+
setFocusedDate(prev => clampDate(prev, lowerBound, max));
|
|
13520
|
+
}, [lowerBound, max]);
|
|
13176
13521
|
const [open, setOpenState] = useState(false);
|
|
13177
13522
|
const [text, setText] = useState(value ? formatDate(value, resolvedFormat, locale) : '');
|
|
13178
13523
|
const inputRef = useRef(null);
|
|
@@ -13230,7 +13575,7 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13230
13575
|
}, [isControlled, onChange, isDayGranularity, granularity]);
|
|
13231
13576
|
// At month or year granularity the bounds widen to the periods holding
|
|
13232
13577
|
// them, so the month or year containing `min` stays reachable.
|
|
13233
|
-
const periodMin = useMemo(() =>
|
|
13578
|
+
const periodMin = useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
|
|
13234
13579
|
const periodMax = useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
|
|
13235
13580
|
const handleSelect = useCallback((d) => {
|
|
13236
13581
|
if (!isWithin(d, periodMin, periodMax))
|
|
@@ -13281,7 +13626,7 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13281
13626
|
]);
|
|
13282
13627
|
// Typing, and the re-parse on blur or Enter, that lands in the period the
|
|
13283
13628
|
// value already holds commits nothing, so a `value` elsewhere in its month
|
|
13284
|
-
// or year
|
|
13629
|
+
// or year changes only when the user picks or types a different period.
|
|
13285
13630
|
const commitTyped = useCallback((next) => {
|
|
13286
13631
|
if (next &&
|
|
13287
13632
|
value &&
|
|
@@ -13347,7 +13692,7 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13347
13692
|
? fromIsoValue(e.target.value, granularity)
|
|
13348
13693
|
: null;
|
|
13349
13694
|
commitValue(parsed);
|
|
13350
|
-
}, min: min ? toIsoValue(
|
|
13695
|
+
}, min: min ? toIsoValue(lowerBound, granularity) : undefined, max: max ? toIsoValue(max, granularity) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
|
|
13351
13696
|
}
|
|
13352
13697
|
const chooseLabel = granularity === 'month'
|
|
13353
13698
|
? t.chooseMonth
|
|
@@ -13356,7 +13701,11 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13356
13701
|
: t.chooseDate;
|
|
13357
13702
|
const calendar = (jsx(Calendar
|
|
13358
13703
|
// A new granularity starts the calendar over on its own view.
|
|
13359
|
-
, { granularity: granularity, value: value, focusedDate: focusedDate, onSelect: handleSelect, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size,
|
|
13704
|
+
, { 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,
|
|
13705
|
+
// The popover panel takes `popoverId`, so the calendar inside it
|
|
13706
|
+
// takes one of its own. Inline there is no panel, and the calendar
|
|
13707
|
+
// keeps the id it released with.
|
|
13708
|
+
id: inline ? popoverId : `${popoverId}-cal`, autoFocusCell: open, labels: labels }, granularity));
|
|
13360
13709
|
if (inline) {
|
|
13361
13710
|
return (jsxs("div", { ...cleanRest, ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), children: [calendar, name && (jsx("input", { type: "hidden", name: name, form: form, value: value ? toIsoValue(value, granularity) : '', required: required }))] }));
|
|
13362
13711
|
}
|
|
@@ -13384,29 +13733,57 @@ DateInputBase.displayName = 'DateInputBase';
|
|
|
13384
13733
|
* shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6}
|
|
13385
13734
|
* />
|
|
13386
13735
|
*/
|
|
13387
|
-
const DateInput = forwardRef((
|
|
13736
|
+
const DateInput = forwardRef((props, ref) => {
|
|
13737
|
+
const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
|
|
13388
13738
|
const insideField = useInsideField();
|
|
13389
13739
|
const insideControl = useInsideControl();
|
|
13740
|
+
const ownField = rendersOwnField('DateInput', {
|
|
13741
|
+
insideField,
|
|
13742
|
+
insideControl,
|
|
13743
|
+
label,
|
|
13744
|
+
message,
|
|
13745
|
+
horizontal,
|
|
13746
|
+
fieldClassName,
|
|
13747
|
+
});
|
|
13748
|
+
// Inline mode renders a bare calendar with no input, so the Control's
|
|
13749
|
+
// icon-left container has nothing to anchor to, and it renders no
|
|
13750
|
+
// Control inside one or out. The left icon is checked as the caller
|
|
13751
|
+
// passed it: the default glyph is this component's own choice, not a
|
|
13752
|
+
// prop the caller set.
|
|
13753
|
+
const ownControl = rendersOwnControl('DateInput', {
|
|
13754
|
+
insideControl,
|
|
13755
|
+
inline: baseProps.inline,
|
|
13756
|
+
isLoading,
|
|
13757
|
+
iconLeft,
|
|
13758
|
+
iconLeftName: props.iconLeftName,
|
|
13759
|
+
iconLeftSize,
|
|
13760
|
+
iconRight,
|
|
13761
|
+
iconRightName,
|
|
13762
|
+
iconRightSize,
|
|
13763
|
+
hasIconsLeft,
|
|
13764
|
+
hasIconsRight,
|
|
13765
|
+
isExpanded,
|
|
13766
|
+
controlSize,
|
|
13767
|
+
controlClassName,
|
|
13768
|
+
});
|
|
13390
13769
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
13391
13770
|
label,
|
|
13392
13771
|
id: baseProps.id,
|
|
13393
13772
|
labelProps,
|
|
13394
13773
|
// Inline mode renders a bare calendar with no input to label.
|
|
13395
|
-
rendersLabel:
|
|
13774
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
13396
13775
|
});
|
|
13397
13776
|
const helpClass = usePrefixedClassNames('help', {
|
|
13398
13777
|
[`is-${messageColor}`]: !!messageColor,
|
|
13399
13778
|
});
|
|
13400
|
-
// The
|
|
13401
|
-
//
|
|
13402
|
-
let content = (jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps
|
|
13403
|
-
|
|
13404
|
-
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
13405
|
-
if (!insideControl && !baseProps.inline) {
|
|
13779
|
+
// The base hides its launcher while the Control it sits in is loading,
|
|
13780
|
+
// whether that is the one rendered below or an enclosing one.
|
|
13781
|
+
let content = (jsx(DateInputBase, { ref: ref, id: controlId, ...baseProps }));
|
|
13782
|
+
if (ownControl) {
|
|
13406
13783
|
content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
13407
13784
|
}
|
|
13408
13785
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
13409
|
-
if (
|
|
13786
|
+
if (ownField) {
|
|
13410
13787
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
13411
13788
|
}
|
|
13412
13789
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
@@ -14280,7 +14657,10 @@ const fromIsoTime = (s) => {
|
|
|
14280
14657
|
* @returns {JSX.Element}
|
|
14281
14658
|
*/
|
|
14282
14659
|
const TimeInputBase = forwardRef((props, ref) => {
|
|
14283
|
-
const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, hourFormat = '24', enableSeconds = false, incrementHours = 1, incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon
|
|
14660
|
+
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;
|
|
14661
|
+
// The launcher gives way to the loading spinner of a Control this sits in.
|
|
14662
|
+
const controlLoading = useControlLoading();
|
|
14663
|
+
const triggerIcon = triggerIconProp ?? !controlLoading;
|
|
14284
14664
|
// Platform-appropriate feedback routing: when `haptics` is opted in and
|
|
14285
14665
|
// the runtime has no Vibration API (notably iOS Safari), enable the
|
|
14286
14666
|
// audio thunk so the user still gets *some* tactile cue. When vibrate
|
|
@@ -14357,6 +14737,8 @@ const TimeInputBase = forwardRef((props, ref) => {
|
|
|
14357
14737
|
useEffect(() => {
|
|
14358
14738
|
setText(value ? formatTime(value, defaultFormat, locale) : '');
|
|
14359
14739
|
}, [value, defaultFormat, locale]);
|
|
14740
|
+
// A value's date before year 1 is out of range, as in the date pickers.
|
|
14741
|
+
const lowerBound = useMemo(() => floorMin(min), [min]);
|
|
14360
14742
|
const commitValue = useCallback((next) => {
|
|
14361
14743
|
if (!isControlled)
|
|
14362
14744
|
setInternalValue(next);
|
|
@@ -14365,10 +14747,10 @@ const TimeInputBase = forwardRef((props, ref) => {
|
|
|
14365
14747
|
const handleSpinnerChange = useCallback((parts) => {
|
|
14366
14748
|
const base = value ?? new Date();
|
|
14367
14749
|
const next = setTimeOfDay(base, parts);
|
|
14368
|
-
if (!isWithin(next,
|
|
14750
|
+
if (!isWithin(next, lowerBound, max))
|
|
14369
14751
|
return;
|
|
14370
14752
|
commitValue(next);
|
|
14371
|
-
}, [value,
|
|
14753
|
+
}, [value, lowerBound, max, commitValue]);
|
|
14372
14754
|
const tryParse = useCallback((s) => {
|
|
14373
14755
|
const trimmed = s.trim();
|
|
14374
14756
|
if (!trimmed)
|
|
@@ -14400,7 +14782,7 @@ const TimeInputBase = forwardRef((props, ref) => {
|
|
|
14400
14782
|
setText,
|
|
14401
14783
|
makeBaseDate,
|
|
14402
14784
|
locale,
|
|
14403
|
-
min,
|
|
14785
|
+
min: lowerBound,
|
|
14404
14786
|
max,
|
|
14405
14787
|
isBlocked: unselectableTimes,
|
|
14406
14788
|
disabled,
|
|
@@ -14479,37 +14861,69 @@ TimeInputBase.displayName = 'TimeInputBase';
|
|
|
14479
14861
|
* @example
|
|
14480
14862
|
* <TimeInput label="Slot" hourFormat="12" incrementMinutes={15} />
|
|
14481
14863
|
*/
|
|
14482
|
-
const TimeInput = forwardRef((
|
|
14864
|
+
const TimeInput = forwardRef((props, ref) => {
|
|
14865
|
+
const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'clock', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
|
|
14483
14866
|
const insideField = useInsideField();
|
|
14484
14867
|
const insideControl = useInsideControl();
|
|
14868
|
+
const ownField = rendersOwnField('TimeInput', {
|
|
14869
|
+
insideField,
|
|
14870
|
+
insideControl,
|
|
14871
|
+
label,
|
|
14872
|
+
message,
|
|
14873
|
+
horizontal,
|
|
14874
|
+
fieldClassName,
|
|
14875
|
+
});
|
|
14876
|
+
// Inline mode renders a bare panel with no input, so the Control's
|
|
14877
|
+
// icon-left container has nothing to anchor to, and it renders no
|
|
14878
|
+
// Control inside one or out. The left icon is checked as the caller
|
|
14879
|
+
// passed it: the default glyph is this component's own choice, not a
|
|
14880
|
+
// prop the caller set.
|
|
14881
|
+
const ownControl = rendersOwnControl('TimeInput', {
|
|
14882
|
+
insideControl,
|
|
14883
|
+
inline: baseProps.inline,
|
|
14884
|
+
isLoading,
|
|
14885
|
+
iconLeft,
|
|
14886
|
+
iconLeftName: props.iconLeftName,
|
|
14887
|
+
iconLeftSize,
|
|
14888
|
+
iconRight,
|
|
14889
|
+
iconRightName,
|
|
14890
|
+
iconRightSize,
|
|
14891
|
+
hasIconsLeft,
|
|
14892
|
+
hasIconsRight,
|
|
14893
|
+
isExpanded,
|
|
14894
|
+
controlSize,
|
|
14895
|
+
controlClassName,
|
|
14896
|
+
});
|
|
14485
14897
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
14486
14898
|
label,
|
|
14487
14899
|
id: baseProps.id,
|
|
14488
14900
|
labelProps,
|
|
14489
14901
|
// Inline mode renders a bare panel with no input to label.
|
|
14490
|
-
rendersLabel:
|
|
14902
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
14491
14903
|
});
|
|
14492
14904
|
const helpClass = usePrefixedClassNames('help', {
|
|
14493
14905
|
[`is-${messageColor}`]: !!messageColor,
|
|
14494
14906
|
});
|
|
14495
|
-
// The
|
|
14496
|
-
//
|
|
14497
|
-
let content = (jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps
|
|
14498
|
-
|
|
14499
|
-
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
14500
|
-
if (!insideControl && !baseProps.inline) {
|
|
14907
|
+
// The base hides its launcher while the Control it sits in is loading,
|
|
14908
|
+
// whether that is the one rendered below or an enclosing one.
|
|
14909
|
+
let content = (jsx(TimeInputBase, { ref: ref, id: controlId, ...baseProps }));
|
|
14910
|
+
if (ownControl) {
|
|
14501
14911
|
content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
14502
14912
|
}
|
|
14503
14913
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
14504
|
-
if (
|
|
14914
|
+
if (ownField) {
|
|
14505
14915
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
14506
14916
|
}
|
|
14507
14917
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
14508
14918
|
});
|
|
14509
14919
|
TimeInput.displayName = 'TimeInput';
|
|
14510
14920
|
|
|
14511
|
-
// The year is padded to four digits, as `datetime-local` requires.
|
|
14921
|
+
// The year is padded to four digits, as `datetime-local` requires. HTML has
|
|
14922
|
+
// no such shape for a year before 1, so a date then is empty, as that input
|
|
14923
|
+
// would make it.
|
|
14512
14924
|
const toIsoDateTime = (d, withSeconds) => {
|
|
14925
|
+
if (d.getFullYear() < FIRST_YEAR)
|
|
14926
|
+
return '';
|
|
14513
14927
|
const yyyy = String(d.getFullYear()).padStart(4, '0');
|
|
14514
14928
|
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
|
14515
14929
|
const dd = String(d.getDate()).padStart(2, '0');
|
|
@@ -14542,7 +14956,10 @@ const fromIsoDateTime = (s) => {
|
|
|
14542
14956
|
const DateTimeInputBase = forwardRef((props, ref) => {
|
|
14543
14957
|
const { value: controlledValue, defaultValue, onChange, onOpen, onClose, min, max, disabled, readOnly, placeholder, format, parse, locale, inline = false, mobileNative = 'auto', editable = true, popover = true, openOnFocus = true, closeOnSelect = false, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, hourFormat = '24', enableSeconds = false, incrementHours = 1,
|
|
14544
14958
|
// Step between visible wheel values; 1 gives the iOS-style every-minute wheel.
|
|
14545
|
-
incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, className, name, form, required, id, onFocus, onClick, onKeyDown, onBlur, iconLeftName: _iconLeftName, triggerIcon
|
|
14959
|
+
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;
|
|
14960
|
+
// The launcher gives way to the loading spinner of a Control this sits in.
|
|
14961
|
+
const controlLoading = useControlLoading();
|
|
14962
|
+
const triggerIcon = triggerIconProp ?? !controlLoading;
|
|
14546
14963
|
const t = mergeLabels(labels);
|
|
14547
14964
|
// Platform-appropriate feedback routing for the time wheel: on iOS (no
|
|
14548
14965
|
// navigator.vibrate) the audio thunk fills in; on Android the real haptic
|
|
@@ -14552,14 +14969,16 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14552
14969
|
const isControlled = controlledValue !== undefined;
|
|
14553
14970
|
const [internalValue, setInternalValue] = useState(defaultValue ?? null);
|
|
14554
14971
|
const value = isControlled ? (controlledValue ?? null) : internalValue;
|
|
14555
|
-
|
|
14972
|
+
// Nothing before year 1 is in range, as in HTML's date inputs.
|
|
14973
|
+
const lowerBound = useMemo(() => floorMin(min), [min]);
|
|
14974
|
+
const initialFocused = useMemo(() => clampDate(value ?? new Date(), lowerBound, max),
|
|
14556
14975
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14557
14976
|
[]);
|
|
14558
14977
|
const [focusedDate, setFocusedDate] = useState(initialFocused);
|
|
14559
14978
|
// Re-clamp focusedDate when min/max change so the focused cell stays valid.
|
|
14560
14979
|
useEffect(() => {
|
|
14561
|
-
setFocusedDate(prev => clampDate(prev,
|
|
14562
|
-
}, [
|
|
14980
|
+
setFocusedDate(prev => clampDate(prev, lowerBound, max));
|
|
14981
|
+
}, [lowerBound, max]);
|
|
14563
14982
|
const defaultFormat = format ??
|
|
14564
14983
|
(hourFormat === '12'
|
|
14565
14984
|
? enableSeconds
|
|
@@ -14656,18 +15075,18 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14656
15075
|
minutes: value?.getMinutes() ?? 0,
|
|
14657
15076
|
seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
|
|
14658
15077
|
});
|
|
14659
|
-
if (!isWithin(merged,
|
|
15078
|
+
if (!isWithin(merged, lowerBound, max))
|
|
14660
15079
|
return;
|
|
14661
15080
|
commitValue(merged);
|
|
14662
15081
|
setFocusedDate(d);
|
|
14663
|
-
}, [value, enableSeconds,
|
|
15082
|
+
}, [value, enableSeconds, lowerBound, max, commitValue]);
|
|
14664
15083
|
const handleTimeChange = useCallback((parts) => {
|
|
14665
15084
|
const base = value ?? focusedDate;
|
|
14666
15085
|
const next = setTimeOfDay(base, parts);
|
|
14667
|
-
if (!isWithin(next,
|
|
15086
|
+
if (!isWithin(next, lowerBound, max))
|
|
14668
15087
|
return;
|
|
14669
15088
|
commitValue(next);
|
|
14670
|
-
}, [value, focusedDate,
|
|
15089
|
+
}, [value, focusedDate, lowerBound, max, commitValue]);
|
|
14671
15090
|
const tryParse = useCallback((s) => {
|
|
14672
15091
|
const trimmed = s.trim();
|
|
14673
15092
|
if (!trimmed)
|
|
@@ -14702,7 +15121,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14702
15121
|
setText,
|
|
14703
15122
|
makeBaseDate,
|
|
14704
15123
|
locale,
|
|
14705
|
-
min,
|
|
15124
|
+
min: lowerBound,
|
|
14706
15125
|
max,
|
|
14707
15126
|
isBlocked,
|
|
14708
15127
|
disabled,
|
|
@@ -14735,7 +15154,7 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
14735
15154
|
? fromIsoDateTime(e.target.value)
|
|
14736
15155
|
: null;
|
|
14737
15156
|
commitValue(parsed);
|
|
14738
|
-
}, min: min ? toIsoDateTime(
|
|
15157
|
+
}, min: min ? toIsoDateTime(lowerBound, enableSeconds) : undefined, max: max ? toIsoDateTime(max, enableSeconds) : undefined, disabled: disabled, readOnly: readOnly, placeholder: placeholder, name: name, form: form, required: required, id: id }));
|
|
14739
15158
|
}
|
|
14740
15159
|
const spinnerValue = {
|
|
14741
15160
|
hours: value?.getHours() ?? 0,
|
|
@@ -14783,29 +15202,57 @@ DateTimeInputBase.displayName = 'DateTimeInputBase';
|
|
|
14783
15202
|
* shouldDisableDate={d => d.getDay() === 0 || d.getDay() === 6}
|
|
14784
15203
|
* />
|
|
14785
15204
|
*/
|
|
14786
|
-
const DateTimeInput = forwardRef((
|
|
15205
|
+
const DateTimeInput = forwardRef((props, ref) => {
|
|
15206
|
+
const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar-alt', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
|
|
14787
15207
|
const insideField = useInsideField();
|
|
14788
15208
|
const insideControl = useInsideControl();
|
|
15209
|
+
const ownField = rendersOwnField('DateTimeInput', {
|
|
15210
|
+
insideField,
|
|
15211
|
+
insideControl,
|
|
15212
|
+
label,
|
|
15213
|
+
message,
|
|
15214
|
+
horizontal,
|
|
15215
|
+
fieldClassName,
|
|
15216
|
+
});
|
|
15217
|
+
// Inline mode renders a bare picker with no input, so the Control's
|
|
15218
|
+
// icon-left container has nothing to anchor to, and it renders no
|
|
15219
|
+
// Control inside one or out. The left icon is checked as the caller
|
|
15220
|
+
// passed it: the default glyph is this component's own choice, not a
|
|
15221
|
+
// prop the caller set.
|
|
15222
|
+
const ownControl = rendersOwnControl('DateTimeInput', {
|
|
15223
|
+
insideControl,
|
|
15224
|
+
inline: baseProps.inline,
|
|
15225
|
+
isLoading,
|
|
15226
|
+
iconLeft,
|
|
15227
|
+
iconLeftName: props.iconLeftName,
|
|
15228
|
+
iconLeftSize,
|
|
15229
|
+
iconRight,
|
|
15230
|
+
iconRightName,
|
|
15231
|
+
iconRightSize,
|
|
15232
|
+
hasIconsLeft,
|
|
15233
|
+
hasIconsRight,
|
|
15234
|
+
isExpanded,
|
|
15235
|
+
controlSize,
|
|
15236
|
+
controlClassName,
|
|
15237
|
+
});
|
|
14789
15238
|
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
14790
15239
|
label,
|
|
14791
15240
|
id: baseProps.id,
|
|
14792
15241
|
labelProps,
|
|
14793
15242
|
// Inline mode renders a bare picker with no input to label.
|
|
14794
|
-
rendersLabel:
|
|
15243
|
+
rendersLabel: ownField && !baseProps.inline,
|
|
14795
15244
|
});
|
|
14796
15245
|
const helpClass = usePrefixedClassNames('help', {
|
|
14797
15246
|
[`is-${messageColor}`]: !!messageColor,
|
|
14798
15247
|
});
|
|
14799
|
-
// The
|
|
14800
|
-
//
|
|
14801
|
-
let content = (jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps
|
|
14802
|
-
|
|
14803
|
-
// icon-left container has nothing to anchor to. Skip the Control wrap.
|
|
14804
|
-
if (!insideControl && !baseProps.inline) {
|
|
15248
|
+
// The base hides its launcher while the Control it sits in is loading,
|
|
15249
|
+
// whether that is the one rendered below or an enclosing one.
|
|
15250
|
+
let content = (jsx(DateTimeInputBase, { ref: ref, id: controlId, ...baseProps }));
|
|
15251
|
+
if (ownControl) {
|
|
14805
15252
|
content = (jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft || !!iconLeftName, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
14806
15253
|
}
|
|
14807
15254
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
14808
|
-
if (
|
|
15255
|
+
if (ownField) {
|
|
14809
15256
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
14810
15257
|
}
|
|
14811
15258
|
return (jsxs(Fragment, { children: [content, messageEl] }));
|