@allxsmith/bestax-bulma 5.26.6 → 5.27.0
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 +752 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +751 -80
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/Tabs.d.ts +25 -3
- package/dist/types/form/DateInputBase.d.ts +5 -2
- package/dist/types/form/DateRangeInput.d.ts +91 -0
- package/dist/types/form/DateRangeInputBase.d.ts +172 -0
- package/dist/types/form/DateTimeInputBase.d.ts +7 -2
- package/dist/types/form/File.d.ts +15 -2
- package/dist/types/form/FormContext.d.ts +11 -0
- package/dist/types/form/TimeInputBase.d.ts +1 -1
- package/dist/types/form/_pickerInternals/PickerPopover.d.ts +1 -0
- package/dist/types/form/_pickerInternals/dateUtils.d.ts +7 -0
- package/dist/types/form/_pickerInternals/formatters.d.ts +14 -0
- package/dist/types/form/_pickerInternals/pickerTypes.d.ts +13 -0
- package/dist/types/form/_pickerInternals/useSegmentedEntry.d.ts +12 -3
- package/dist/types/index.d.ts +4 -0
- package/dist/types-cjs/components/Tabs.d.ts +25 -3
- package/dist/types-cjs/form/DateInputBase.d.ts +5 -2
- package/dist/types-cjs/form/DateRangeInput.d.ts +91 -0
- package/dist/types-cjs/form/DateRangeInputBase.d.ts +172 -0
- package/dist/types-cjs/form/DateTimeInputBase.d.ts +7 -2
- package/dist/types-cjs/form/File.d.ts +15 -2
- package/dist/types-cjs/form/FormContext.d.ts +11 -0
- package/dist/types-cjs/form/TimeInputBase.d.ts +1 -1
- package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +1 -0
- package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +7 -0
- package/dist/types-cjs/form/_pickerInternals/formatters.d.ts +14 -0
- package/dist/types-cjs/form/_pickerInternals/pickerTypes.d.ts +13 -0
- package/dist/types-cjs/form/_pickerInternals/useSegmentedEntry.d.ts +12 -3
- package/dist/types-cjs/index.d.ts +4 -0
- 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/components/_tabs.scss +44 -0
- package/src/scss/form/_dateinput.scss +50 -2
- package/src/scss/form/_daterangeinput.scss +63 -0
- package/src/scss/form/_file.scss +37 -0
- package/src/scss/form/_index.scss +2 -0
package/dist/index.esm.js
CHANGED
|
@@ -4139,9 +4139,65 @@ const TabsContext = createContext(null);
|
|
|
4139
4139
|
function useTabsContext() {
|
|
4140
4140
|
return useContext(TabsContext);
|
|
4141
4141
|
}
|
|
4142
|
+
/**
|
|
4143
|
+
* The tabs and panels currently mounted under one `Tabs`. Each registers from
|
|
4144
|
+
* an effect and removes itself on cleanup, so the links between them only
|
|
4145
|
+
* ever name elements that exist, wherever in the tree the panels sit.
|
|
4146
|
+
*/
|
|
4147
|
+
function useRegistry() {
|
|
4148
|
+
const [records, setRecords] = useState([]);
|
|
4149
|
+
const register = useCallback((record) => {
|
|
4150
|
+
setRecords(prev => [...prev, record]);
|
|
4151
|
+
return () => setRecords(prev => prev.filter(r => r !== record));
|
|
4152
|
+
}, []);
|
|
4153
|
+
return [records, register];
|
|
4154
|
+
}
|
|
4155
|
+
/**
|
|
4156
|
+
* The list a tab belongs to: its `role="tablist"` ancestor, so an element
|
|
4157
|
+
* wrapped around a tab doesn't cut it off from the others, or its parent when
|
|
4158
|
+
* it sits outside a tab list.
|
|
4159
|
+
*/
|
|
4160
|
+
function tabListOf(tab) {
|
|
4161
|
+
return (tab.closest('[role="tablist"]') ?? tab.parentElement);
|
|
4162
|
+
}
|
|
4163
|
+
/**
|
|
4164
|
+
* The tab a key press moves focus to, found among the tabs of the same list
|
|
4165
|
+
* in DOM order, skipping disabled ones and wrapping at either end.
|
|
4166
|
+
* `undefined` for a key the tab list does not handle, or when no enabled tab
|
|
4167
|
+
* is left to go to.
|
|
4168
|
+
*/
|
|
4169
|
+
function findTargetTab(current, key, vertical) {
|
|
4170
|
+
const tabs = Array.from(tabListOf(current).querySelectorAll('[role="tab"]'));
|
|
4171
|
+
const isEnabled = (el) => el.getAttribute('aria-disabled') !== 'true';
|
|
4172
|
+
if (key === 'Home')
|
|
4173
|
+
return tabs.find(isEnabled);
|
|
4174
|
+
if (key === 'End')
|
|
4175
|
+
return [...tabs].reverse().find(isEnabled);
|
|
4176
|
+
let step;
|
|
4177
|
+
if (key === 'ArrowRight' || (vertical && key === 'ArrowDown'))
|
|
4178
|
+
step = 1;
|
|
4179
|
+
else if (key === 'ArrowLeft' || (vertical && key === 'ArrowUp'))
|
|
4180
|
+
step = -1;
|
|
4181
|
+
else
|
|
4182
|
+
return undefined;
|
|
4183
|
+
const start = tabs.indexOf(current);
|
|
4184
|
+
for (let i = 1; i <= tabs.length; i++) {
|
|
4185
|
+
const candidate = tabs[(((start + step * i) % tabs.length) + tabs.length) % tabs.length];
|
|
4186
|
+
if (isEnabled(candidate))
|
|
4187
|
+
return candidate;
|
|
4188
|
+
}
|
|
4189
|
+
return undefined;
|
|
4190
|
+
}
|
|
4142
4191
|
/**
|
|
4143
4192
|
* The `Tabs` component provides flexible and fully-featured Bulma tab navigation for your Bulma React UI.
|
|
4144
4193
|
*
|
|
4194
|
+
* Built from `Tabs.Tab`, the tabs follow the WAI-ARIA tabs pattern with
|
|
4195
|
+
* manual activation: the tab list is a single tab stop, the arrow keys, Home
|
|
4196
|
+
* and End move focus between tabs (skipping disabled ones), and Enter or
|
|
4197
|
+
* Space activates the focused tab. Each tab names its `Tabs.Content.Item` in
|
|
4198
|
+
* `aria-controls`, and each panel names its tab in `aria-labelledby`. Built
|
|
4199
|
+
* from `Tabs.Item` with links inside, the tabs stay plain Bulma navigation.
|
|
4200
|
+
*
|
|
4145
4201
|
* @function
|
|
4146
4202
|
* @param {TabsProps} props - Props for the Tabs component.
|
|
4147
4203
|
* @returns {JSX.Element} The rendered tabs component.
|
|
@@ -4173,7 +4229,45 @@ const TabsComponent = ({ align, size, isFullwidth, isFullWidth, fullwidth, boxed
|
|
|
4173
4229
|
}
|
|
4174
4230
|
onChange?.(index);
|
|
4175
4231
|
}, [isControlled, onChange]);
|
|
4176
|
-
const
|
|
4232
|
+
const baseId = useId();
|
|
4233
|
+
const [tabs, registerTab] = useRegistry();
|
|
4234
|
+
const [panels, registerPanel] = useRegistry();
|
|
4235
|
+
const [focusedTab, setFocusedTab] = useState(null);
|
|
4236
|
+
// One tab stop for the whole list (roving tabindex). While focus is inside
|
|
4237
|
+
// the list the stop follows it, so Tab and Shift+Tab leave the list from
|
|
4238
|
+
// whichever tab has focus; once focus leaves, the stop is the selected tab
|
|
4239
|
+
// again. A disabled tab never holds it, and when the selected tab is
|
|
4240
|
+
// disabled or missing the enabled tab with the lowest index does, so the
|
|
4241
|
+
// list can always be reached. Before the tabs register (the server render
|
|
4242
|
+
// and the first client render), the selected tab holds it. The registry is
|
|
4243
|
+
// in the order the tabs last registered, which a tab changing its
|
|
4244
|
+
// `disabled` or `id` reshuffles, hence the sort.
|
|
4245
|
+
const enabledTabs = tabs
|
|
4246
|
+
.filter(t => !t.disabled)
|
|
4247
|
+
.sort((a, b) => a.index - b.index);
|
|
4248
|
+
const canHoldStop = (index) => enabledTabs.some(t => t.index === index);
|
|
4249
|
+
let tabStop;
|
|
4250
|
+
if (canHoldStop(focusedTab))
|
|
4251
|
+
tabStop = focusedTab;
|
|
4252
|
+
else if (tabs.length === 0 || canHoldStop(activeTab))
|
|
4253
|
+
tabStop = activeTab;
|
|
4254
|
+
else
|
|
4255
|
+
tabStop = enabledTabs.length > 0 ? enabledTabs[0].index : null;
|
|
4256
|
+
// Check if children include TabsContent
|
|
4257
|
+
const childArray = React.Children.toArray(children);
|
|
4258
|
+
const hasContent = childArray.some(child => React.isValidElement(child) && child.type === TabsContent);
|
|
4259
|
+
const contextValue = {
|
|
4260
|
+
activeTab,
|
|
4261
|
+
setActiveTab,
|
|
4262
|
+
baseId,
|
|
4263
|
+
tabStop,
|
|
4264
|
+
setFocusedTab,
|
|
4265
|
+
tabId: index => tabs.find(t => t.index === index)?.id,
|
|
4266
|
+
panelId: index => panels.find(p => p.index === index)?.id,
|
|
4267
|
+
registerTab,
|
|
4268
|
+
registerPanel,
|
|
4269
|
+
orientation: hasContent && vertical ? 'vertical' : 'horizontal',
|
|
4270
|
+
};
|
|
4177
4271
|
// Build classes for the .tabs div
|
|
4178
4272
|
const tabsClasses = usePrefixedClassNames('tabs', {
|
|
4179
4273
|
[`is-${align}`]: align,
|
|
@@ -4184,9 +4278,6 @@ const TabsComponent = ({ align, size, isFullwidth, isFullWidth, fullwidth, boxed
|
|
|
4184
4278
|
'is-toggle': toggle,
|
|
4185
4279
|
'is-toggle-rounded': rounded,
|
|
4186
4280
|
});
|
|
4187
|
-
// Check if children include TabsContent
|
|
4188
|
-
const childArray = React.Children.toArray(children);
|
|
4189
|
-
const hasContent = childArray.some(child => React.isValidElement(child) && child.type === TabsContent);
|
|
4190
4281
|
// Hoisted unconditionally to respect rules-of-hooks. Modifiers gate themselves
|
|
4191
4282
|
// via their truthy values — `tabs-root` is always prefixed, modifiers only
|
|
4192
4283
|
// apply in the vertical-with-content branch.
|
|
@@ -4235,26 +4326,82 @@ const TabsComponent = ({ align, size, isFullwidth, isFullWidth, fullwidth, boxed
|
|
|
4235
4326
|
* @param {TabListProps} props - Props for the TabList component.
|
|
4236
4327
|
* @returns {JSX.Element} The rendered tab list.
|
|
4237
4328
|
*/
|
|
4238
|
-
const TabList = ({ className, children, ...props }) =>
|
|
4329
|
+
const TabList = ({ className, children, ...props }) => {
|
|
4330
|
+
const ctx = useTabsContext();
|
|
4331
|
+
return (jsx("ul", { role: "tablist", "aria-orientation": ctx?.orientation === 'vertical' ? 'vertical' : undefined, className: classNames(className) || undefined, ...props, children: children }));
|
|
4332
|
+
};
|
|
4239
4333
|
/**
|
|
4240
4334
|
* Individual tab button. Consumes Tabs context for active state management.
|
|
4241
4335
|
* Renders `<a>` internally — consumers provide only the label text/children.
|
|
4336
|
+
* Inside a `Tabs` it takes part in the tab list's keyboard support and links
|
|
4337
|
+
* to the `Tabs.Content.Item` with the same `index`.
|
|
4242
4338
|
*
|
|
4243
4339
|
* @function
|
|
4244
4340
|
* @param {TabProps} props - Props for the Tab component.
|
|
4245
4341
|
* @returns {JSX.Element} The rendered tab.
|
|
4246
4342
|
*/
|
|
4247
|
-
const Tab = ({ index, disabled, icon, iconLibrary, iconVariant, iconSize = 'small', iconFeatures, className, children, ...props }) => {
|
|
4343
|
+
const Tab = ({ index, disabled, icon, iconLibrary, iconVariant, iconSize = 'small', iconFeatures, className, children, id, onKeyDown, onFocus, onBlur, ...props }) => {
|
|
4248
4344
|
const ctx = useTabsContext();
|
|
4249
4345
|
const isActive = ctx ? ctx.activeTab === index : false;
|
|
4346
|
+
const anchorRef = useRef(null);
|
|
4250
4347
|
const activeClass = usePrefixedClassNames({ 'is-active': isActive });
|
|
4348
|
+
const tabId = id ?? (ctx ? `${ctx.baseId}-tab-${index}` : undefined);
|
|
4349
|
+
const registerTab = ctx?.registerTab;
|
|
4350
|
+
useEffect(() => {
|
|
4351
|
+
if (!registerTab || tabId === undefined)
|
|
4352
|
+
return undefined;
|
|
4353
|
+
return registerTab({ index, id: tabId, disabled: !!disabled });
|
|
4354
|
+
}, [registerTab, index, tabId, disabled]);
|
|
4355
|
+
// Outside a Tabs there is no list to rove, so every enabled tab stays in
|
|
4356
|
+
// the page tab order, as it always did.
|
|
4357
|
+
let tabIndex;
|
|
4358
|
+
if (disabled)
|
|
4359
|
+
tabIndex = -1;
|
|
4360
|
+
else if (ctx)
|
|
4361
|
+
tabIndex = ctx.tabStop === index ? 0 : -1;
|
|
4362
|
+
else
|
|
4363
|
+
tabIndex = 0;
|
|
4251
4364
|
const handleClick = useCallback((e) => {
|
|
4252
4365
|
e.preventDefault();
|
|
4253
4366
|
if (!disabled && ctx) {
|
|
4254
4367
|
ctx.setActiveTab(index);
|
|
4255
4368
|
}
|
|
4256
4369
|
}, [disabled, ctx, index]);
|
|
4257
|
-
|
|
4370
|
+
const handleKeyDown = (e) => {
|
|
4371
|
+
onKeyDown?.(e);
|
|
4372
|
+
// A key on something inside the tab is that element's to answer.
|
|
4373
|
+
if (e.defaultPrevented ||
|
|
4374
|
+
e.target !== e.currentTarget ||
|
|
4375
|
+
e.altKey ||
|
|
4376
|
+
e.ctrlKey ||
|
|
4377
|
+
e.metaKey) {
|
|
4378
|
+
return;
|
|
4379
|
+
}
|
|
4380
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
4381
|
+
e.preventDefault();
|
|
4382
|
+
// Activate through a click on the inner <a>, the element a pointer
|
|
4383
|
+
// clicks, so click listeners see keyboard activation the way they see
|
|
4384
|
+
// a native button's.
|
|
4385
|
+
anchorRef.current.click();
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
const target = findTargetTab(e.currentTarget, e.key, ctx?.orientation === 'vertical');
|
|
4389
|
+
if (target) {
|
|
4390
|
+
e.preventDefault();
|
|
4391
|
+
target.focus();
|
|
4392
|
+
}
|
|
4393
|
+
};
|
|
4394
|
+
const handleFocus = (e) => {
|
|
4395
|
+
onFocus?.(e);
|
|
4396
|
+
ctx?.setFocusedTab(index);
|
|
4397
|
+
};
|
|
4398
|
+
const handleBlur = (e) => {
|
|
4399
|
+
onBlur?.(e);
|
|
4400
|
+
if (!tabListOf(e.currentTarget).contains(e.relatedTarget)) {
|
|
4401
|
+
ctx?.setFocusedTab(null);
|
|
4402
|
+
}
|
|
4403
|
+
};
|
|
4404
|
+
return (jsx("li", { className: classNames(activeClass, className) || undefined, role: "tab", id: tabId, "aria-selected": isActive, "aria-controls": ctx?.panelId(index), "aria-disabled": disabled || undefined, tabIndex: tabIndex, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, ...props, children: jsxs("a", { ref: anchorRef, onClick: handleClick, "aria-disabled": disabled || undefined, children: [icon && (jsx(Icon, { name: icon, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })), children && jsx("span", { children: children })] }) }));
|
|
4258
4405
|
};
|
|
4259
4406
|
/**
|
|
4260
4407
|
* Each tab; accepts `active`, `onClick`, etc.
|
|
@@ -4284,13 +4431,20 @@ const TabsContentComponent = ({ className, children, ...props }) => {
|
|
|
4284
4431
|
* @param {TabContentItemProps} props - Props for the TabContentItem component.
|
|
4285
4432
|
* @returns {JSX.Element} The rendered tab content panel.
|
|
4286
4433
|
*/
|
|
4287
|
-
const TabContentItem = ({ index, className, children, ...props }) => {
|
|
4434
|
+
const TabContentItem = ({ index, className, children, id, ...props }) => {
|
|
4288
4435
|
const ctx = useTabsContext();
|
|
4289
4436
|
const isActive = ctx ? ctx.activeTab === index : false;
|
|
4290
4437
|
const itemClass = usePrefixedClassNames('tabs-content-item', {
|
|
4291
4438
|
'is-active': isActive,
|
|
4292
4439
|
});
|
|
4293
|
-
|
|
4440
|
+
const panelId = id ?? (ctx ? `${ctx.baseId}-panel-${index}` : undefined);
|
|
4441
|
+
const registerPanel = ctx?.registerPanel;
|
|
4442
|
+
useEffect(() => {
|
|
4443
|
+
if (!registerPanel || panelId === undefined)
|
|
4444
|
+
return undefined;
|
|
4445
|
+
return registerPanel({ index, id: panelId });
|
|
4446
|
+
}, [registerPanel, index, panelId]);
|
|
4447
|
+
return (jsx("div", { className: classNames(itemClass, className), role: "tabpanel", id: panelId, "aria-labelledby": ctx?.tabId(index), "aria-hidden": !isActive, ...props, children: children }));
|
|
4294
4448
|
};
|
|
4295
4449
|
// ---------------------------------------------------------------------------
|
|
4296
4450
|
// Static property attachment
|
|
@@ -8848,6 +9002,10 @@ const ON_CONTROL = {
|
|
|
8848
9002
|
* only the props it hands its own `Control`, as the caller gave them, so a
|
|
8849
9003
|
* default the wrapper fills in itself never warns. A prop counts when it is
|
|
8850
9004
|
* truthy, since a falsy one would change nothing on a `Control` either.
|
|
9005
|
+
* `iconLeftSize` and `hasIconsLeft` show nothing on a `Control` without a
|
|
9006
|
+
* glyph, so when they move and the caller chose no left icon, the advice
|
|
9007
|
+
* names the wrapper's default glyph too, and following it draws what the
|
|
9008
|
+
* wrapper's own `Control` did.
|
|
8851
9009
|
* Internal; not part of the public API.
|
|
8852
9010
|
*/
|
|
8853
9011
|
const rendersOwnControl = (component, options) => {
|
|
@@ -8858,18 +9016,32 @@ const rendersOwnControl = (component, options) => {
|
|
|
8858
9016
|
return false;
|
|
8859
9017
|
const names = held.join(' ');
|
|
8860
9018
|
const doNothing = held.length > 1 ? 'those props do' : 'that prop does';
|
|
9019
|
+
let message;
|
|
8861
9020
|
if (options.inline) {
|
|
8862
|
-
|
|
8863
|
-
`
|
|
8864
|
-
|
|
8865
|
-
|
|
9021
|
+
message =
|
|
9022
|
+
`[bestax-bulma] <${component} inline ${names}> renders no <Control> ` +
|
|
9023
|
+
`in inline mode, inside a <Control> or not, so ${doNothing} ` +
|
|
9024
|
+
`nothing. Leave ${held.length > 1 ? 'them' : 'it'} out of an ` +
|
|
9025
|
+
`inline picker.`;
|
|
8866
9026
|
}
|
|
8867
9027
|
else {
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
9028
|
+
const toSet = held.map(prop => ON_CONTROL[prop]);
|
|
9029
|
+
const needsGlyph = (options.iconLeftSize || options.hasIconsLeft) &&
|
|
9030
|
+
!options.iconLeft &&
|
|
9031
|
+
options.iconLeftName === undefined &&
|
|
9032
|
+
options.defaultIconLeftName;
|
|
9033
|
+
if (needsGlyph) {
|
|
9034
|
+
toSet.push(`iconLeftName="${options.defaultIconLeftName}" (its default icon)`);
|
|
9035
|
+
}
|
|
9036
|
+
message =
|
|
9037
|
+
`[bestax-bulma] <${component} ${names}> inside a <Control> renders ` +
|
|
9038
|
+
`no <Control> of its own, so ${doNothing} nothing there. Set ` +
|
|
9039
|
+
`${listOf(toSet)} on that <Control> instead.`;
|
|
8872
9040
|
}
|
|
9041
|
+
// Keyed by the message itself: the props named do not settle the advice
|
|
9042
|
+
// (an empty `iconLeftName` and an unset one name the same props), so two
|
|
9043
|
+
// calls that would advise differently must not share a key.
|
|
9044
|
+
warnOnce(message, message);
|
|
8873
9045
|
return false;
|
|
8874
9046
|
};
|
|
8875
9047
|
const FieldLabelIdContext = createContext(undefined);
|
|
@@ -9356,7 +9528,40 @@ const Checkboxes = withSubComponents(CheckboxesComponent, { Checkbox }, 'Checkbo
|
|
|
9356
9528
|
*/
|
|
9357
9529
|
const File = forwardRef(({
|
|
9358
9530
|
// Field props
|
|
9359
|
-
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, ...props }, ref) => {
|
|
9531
|
+
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, isBoxed, isFullwidth, isFullWidth, isRight, isCentered, hasName, buttonLabel, iconLeft, iconRight, className, inputClassName, fileName, pickedFilesLabel, onChange, ...props }, ref) => {
|
|
9532
|
+
// What the user last picked, and the form a reset of which clears it.
|
|
9533
|
+
const [picked, setPicked] = useState();
|
|
9534
|
+
const pickedForm = picked?.form;
|
|
9535
|
+
useEffect(() => {
|
|
9536
|
+
if (!pickedForm)
|
|
9537
|
+
return;
|
|
9538
|
+
let timer;
|
|
9539
|
+
// The reset event fires before the form resets, and a listener after this one can still
|
|
9540
|
+
// cancel it, so the name is cleared a task later, and only if the reset went ahead.
|
|
9541
|
+
const onReset = (event) => {
|
|
9542
|
+
timer = setTimeout(() => {
|
|
9543
|
+
if (!event.defaultPrevented)
|
|
9544
|
+
setPicked(undefined);
|
|
9545
|
+
});
|
|
9546
|
+
};
|
|
9547
|
+
pickedForm.addEventListener('reset', onReset);
|
|
9548
|
+
return () => {
|
|
9549
|
+
pickedForm.removeEventListener('reset', onReset);
|
|
9550
|
+
clearTimeout(timer);
|
|
9551
|
+
};
|
|
9552
|
+
}, [pickedForm]);
|
|
9553
|
+
const handleChange = (event) => {
|
|
9554
|
+
const { files, form } = event.target;
|
|
9555
|
+
setPicked(files?.length
|
|
9556
|
+
? { count: files.length, firstName: files[0].name, form }
|
|
9557
|
+
: undefined);
|
|
9558
|
+
onChange?.(event);
|
|
9559
|
+
};
|
|
9560
|
+
const pickedText = picked &&
|
|
9561
|
+
(picked.count === 1
|
|
9562
|
+
? picked.firstName
|
|
9563
|
+
: (pickedFilesLabel?.(picked.count) ?? `${picked.count} files`));
|
|
9564
|
+
const shownName = fileName ?? pickedText;
|
|
9360
9565
|
const insideField = useInsideField();
|
|
9361
9566
|
const insideControl = useInsideControl();
|
|
9362
9567
|
const ownField = rendersOwnField('File', {
|
|
@@ -9402,7 +9607,7 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
9402
9607
|
[`is-${messageColor}`]: !!messageColor,
|
|
9403
9608
|
});
|
|
9404
9609
|
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
9405
|
-
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 &&
|
|
9610
|
+
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, onChange: handleChange, ...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 && shownName && (jsx("span", { className: prefixedClassNames(classPrefix, 'file-name'), children: shownName }))] }) }));
|
|
9406
9611
|
if (ownField) {
|
|
9407
9612
|
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [fileElement, messageEl] }));
|
|
9408
9613
|
}
|
|
@@ -11479,6 +11684,12 @@ fieldClassName, controlClassName,
|
|
|
11479
11684
|
});
|
|
11480
11685
|
TextArea.displayName = 'TextArea';
|
|
11481
11686
|
|
|
11687
|
+
/**
|
|
11688
|
+
* Whether a key is Alt+ArrowDown or Alt+ArrowUp, which open and close a
|
|
11689
|
+
* picker's popover. The calendar and the time wheels leave both to it rather
|
|
11690
|
+
* than reading them as plain arrows.
|
|
11691
|
+
*/
|
|
11692
|
+
const isPopoverToggleKey = (e) => e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp');
|
|
11482
11693
|
const DEFAULT_PICKER_LABELS = {
|
|
11483
11694
|
prevMonth: 'Previous month',
|
|
11484
11695
|
nextMonth: 'Next month',
|
|
@@ -11490,6 +11701,8 @@ const DEFAULT_PICKER_LABELS = {
|
|
|
11490
11701
|
rangeStart: 'Start date',
|
|
11491
11702
|
rangeEnd: 'End date',
|
|
11492
11703
|
rangePreviewEnd: 'Choose as end date',
|
|
11704
|
+
rangeSeparator: '–',
|
|
11705
|
+
chooseDateRange: 'Choose date range',
|
|
11493
11706
|
hours: 'hours',
|
|
11494
11707
|
minutes: 'minutes',
|
|
11495
11708
|
seconds: 'seconds',
|
|
@@ -11697,6 +11910,12 @@ function addMonths(d, n) {
|
|
|
11697
11910
|
function addYears(d, n) {
|
|
11698
11911
|
return addMonths(d, n * 12);
|
|
11699
11912
|
}
|
|
11913
|
+
/**
|
|
11914
|
+
* A copy of `d` with the given parts of its time of day set and the rest
|
|
11915
|
+
* left as they are. A time started from the clock rather than from a value
|
|
11916
|
+
* should pass the seconds its picker does not show, and the milliseconds,
|
|
11917
|
+
* as `0`, or it keeps the ones the clock had.
|
|
11918
|
+
*/
|
|
11700
11919
|
function setTimeOfDay(d, parts) {
|
|
11701
11920
|
const r = new Date(d);
|
|
11702
11921
|
if (parts.hours !== undefined)
|
|
@@ -11705,6 +11924,8 @@ function setTimeOfDay(d, parts) {
|
|
|
11705
11924
|
r.setMinutes(parts.minutes);
|
|
11706
11925
|
if (parts.seconds !== undefined)
|
|
11707
11926
|
r.setSeconds(parts.seconds);
|
|
11927
|
+
if (parts.milliseconds !== undefined)
|
|
11928
|
+
r.setMilliseconds(parts.milliseconds);
|
|
11708
11929
|
return r;
|
|
11709
11930
|
}
|
|
11710
11931
|
/**
|
|
@@ -11853,6 +12074,36 @@ function formatDateTime(d, fmt, locale) {
|
|
|
11853
12074
|
return formatTokenString(d, f);
|
|
11854
12075
|
return formatWithIntl(d, f, locale);
|
|
11855
12076
|
}
|
|
12077
|
+
/**
|
|
12078
|
+
* The value as a native input and a hidden form input carry it:
|
|
12079
|
+
* `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
|
|
12080
|
+
* `<input type="month">` use. The year is padded to four digits, as those
|
|
12081
|
+
* inputs require and as the `YYYY` token displays it. HTML has no such shape
|
|
12082
|
+
* for a year before 1, so a date then is empty, as those inputs would make it.
|
|
12083
|
+
*/
|
|
12084
|
+
function toIsoValue(d, granularity) {
|
|
12085
|
+
if (d.getFullYear() < FIRST_YEAR)
|
|
12086
|
+
return '';
|
|
12087
|
+
const year = pad(d.getFullYear(), 4);
|
|
12088
|
+
if (granularity === 'year')
|
|
12089
|
+
return year;
|
|
12090
|
+
const month = `${year}-${pad(d.getMonth() + 1)}`;
|
|
12091
|
+
return granularity === 'month' ? month : `${month}-${pad(d.getDate())}`;
|
|
12092
|
+
}
|
|
12093
|
+
/**
|
|
12094
|
+
* Read a native `type="date"` or `type="month"` value back into a Date. HTML
|
|
12095
|
+
* allows a year of four or more digits, as `toIsoValue` writes one past 9999.
|
|
12096
|
+
*/
|
|
12097
|
+
function fromIsoValue(s, granularity) {
|
|
12098
|
+
if (granularity === 'month') {
|
|
12099
|
+
const m = /^(\d{4,})-(\d{2})$/.exec(s);
|
|
12100
|
+
return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
|
|
12101
|
+
}
|
|
12102
|
+
const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
|
|
12103
|
+
if (!m)
|
|
12104
|
+
return null;
|
|
12105
|
+
return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
12106
|
+
}
|
|
11856
12107
|
/**
|
|
11857
12108
|
* Derive the hour cycle (`'12'` or `'24'`) a display format will render, from
|
|
11858
12109
|
* its first hour token: `h`/`hh` → 12-hour with meridiem, `H`/`HH` → 24-hour.
|
|
@@ -12354,6 +12605,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
|
|
|
12354
12605
|
}
|
|
12355
12606
|
}, [min, max, isDateUnselectable, onFocusedDateChange]);
|
|
12356
12607
|
const handleKeyDown = useCallback((e) => {
|
|
12608
|
+
if (isPopoverToggleKey(e))
|
|
12609
|
+
return;
|
|
12357
12610
|
// A key takes the range preview back from the pointer.
|
|
12358
12611
|
setHoverDate(null);
|
|
12359
12612
|
switch (e.key) {
|
|
@@ -12446,6 +12699,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
|
|
|
12446
12699
|
}
|
|
12447
12700
|
}, [min, max, isMonthUnselectable, onFocusedDateChange]);
|
|
12448
12701
|
const handleMonthKeyDown = useCallback((e) => {
|
|
12702
|
+
if (isPopoverToggleKey(e))
|
|
12703
|
+
return;
|
|
12449
12704
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
12450
12705
|
e.preventDefault();
|
|
12451
12706
|
if (!isMonthUnselectable(focusedDate)) {
|
|
@@ -12580,6 +12835,8 @@ const Calendar = ({ value, focusedDate, onSelect, onFocusedDateChange, min: minP
|
|
|
12580
12835
|
}
|
|
12581
12836
|
}, [yearList, disabledYears, onFocusedDateChange, min, max]);
|
|
12582
12837
|
const handleYearListKeyDown = useCallback((e) => {
|
|
12838
|
+
if (isPopoverToggleKey(e))
|
|
12839
|
+
return;
|
|
12583
12840
|
if (isYearGranularity) {
|
|
12584
12841
|
const step = yearStep(e.key, focusedYear, yearList);
|
|
12585
12842
|
if (!step)
|
|
@@ -12841,6 +13098,8 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
|
|
|
12841
13098
|
document.addEventListener('pointerdown', handler);
|
|
12842
13099
|
return () => document.removeEventListener('pointerdown', handler);
|
|
12843
13100
|
}, [isOpen, closeOnClickOutside, anchorRef, onClose]);
|
|
13101
|
+
// Escape closes it, and so does Alt+ArrowUp, the combobox's key for it,
|
|
13102
|
+
// which the calendar and the time wheels inside leave to it.
|
|
12844
13103
|
useEffect(() => {
|
|
12845
13104
|
if (!isOpen || !closeOnEscape || !isBrowser)
|
|
12846
13105
|
return undefined;
|
|
@@ -12849,6 +13108,11 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
|
|
|
12849
13108
|
e.stopPropagation();
|
|
12850
13109
|
onClose();
|
|
12851
13110
|
}
|
|
13111
|
+
else if (e.altKey && e.key === 'ArrowUp') {
|
|
13112
|
+
e.preventDefault();
|
|
13113
|
+
e.stopPropagation();
|
|
13114
|
+
onClose();
|
|
13115
|
+
}
|
|
12852
13116
|
};
|
|
12853
13117
|
document.addEventListener('keydown', handler);
|
|
12854
13118
|
return () => document.removeEventListener('keydown', handler);
|
|
@@ -13252,7 +13516,7 @@ const SEPARATOR_RE = /[/:\-.\s]/;
|
|
|
13252
13516
|
* null and the input transparently falls back to free-form text entry.
|
|
13253
13517
|
*/
|
|
13254
13518
|
function useSegmentedEntry(params) {
|
|
13255
|
-
const { format, skipKinds, value, commitValue, formatFn, tryParse, text, setText, makeBaseDate, locale, min, max, isBlocked, disabled, readOnly, editable = true, popover = true, openOnFocus = true, closeOnSelect = false, isOpen, setOpen, inputRef, containerRef, onFocus, onClick, onKeyDown, onBlur, } = params;
|
|
13519
|
+
const { format, skipKinds, value, commitValue, formatFn, tryParse, text, setText, makeBaseDate, locale, min, max, isBlocked, isUnfinishedBlocked, disabled, readOnly, editable = true, popover = true, openOnFocus = true, closeOnSelect = false, isOpen, setOpen, inputRef, containerRef, onFocus, onClick, onKeyDown, onBlur, } = params;
|
|
13256
13520
|
const segmentMap = useMemo(() => {
|
|
13257
13521
|
const map = typeof format === 'string' ? buildSegmentMap(format) : null;
|
|
13258
13522
|
if (!map || !skipKinds)
|
|
@@ -13448,7 +13712,7 @@ function useSegmentedEntry(params) {
|
|
|
13448
13712
|
// Clicking the field opens the popover only when `openOnFocus` is on —
|
|
13449
13713
|
// a click is just a focus. With `openOnFocus={false}` (manual-entry
|
|
13450
13714
|
// mode) the click positions the caret for typing and the popover is
|
|
13451
|
-
// opened explicitly via the right launcher button (or ArrowDown).
|
|
13715
|
+
// opened explicitly via the right launcher button (or Alt+ArrowDown).
|
|
13452
13716
|
if (openOnFocus && popover && !disabled && !readOnly)
|
|
13453
13717
|
setOpen(true);
|
|
13454
13718
|
if (segmentEditable && segmentMap && inputRef.current) {
|
|
@@ -13484,14 +13748,23 @@ function useSegmentedEntry(params) {
|
|
|
13484
13748
|
const isPm = base.getHours() >= 12;
|
|
13485
13749
|
switch (e.key) {
|
|
13486
13750
|
case 'ArrowUp':
|
|
13487
|
-
e.preventDefault();
|
|
13488
|
-
typedDigitsRef.current = '';
|
|
13489
|
-
applyDateFromSegment(incrementSegmentValue(seg, base, 1, isPm));
|
|
13490
|
-
return;
|
|
13491
13751
|
case 'ArrowDown':
|
|
13492
13752
|
e.preventDefault();
|
|
13753
|
+
// The plain arrows step the segment, so the popover opens on
|
|
13754
|
+
// Alt+ArrowDown and closes on Alt+ArrowUp, as a combobox's does,
|
|
13755
|
+
// and neither steps it.
|
|
13756
|
+
if (e.altKey) {
|
|
13757
|
+
if (e.key === 'ArrowUp') {
|
|
13758
|
+
if (isOpen)
|
|
13759
|
+
setOpen(false);
|
|
13760
|
+
}
|
|
13761
|
+
else if (!isOpen && popover) {
|
|
13762
|
+
setOpen(true);
|
|
13763
|
+
}
|
|
13764
|
+
return;
|
|
13765
|
+
}
|
|
13493
13766
|
typedDigitsRef.current = '';
|
|
13494
|
-
applyDateFromSegment(incrementSegmentValue(seg, base, -1, isPm));
|
|
13767
|
+
applyDateFromSegment(incrementSegmentValue(seg, base, e.key === 'ArrowUp' ? 1 : -1, isPm));
|
|
13495
13768
|
return;
|
|
13496
13769
|
case 'ArrowRight':
|
|
13497
13770
|
e.preventDefault();
|
|
@@ -13557,6 +13830,8 @@ function useSegmentedEntry(params) {
|
|
|
13557
13830
|
const nextBuffer = typedDigitsRef.current + e.key;
|
|
13558
13831
|
typedDigitsRef.current = nextBuffer;
|
|
13559
13832
|
const { date, advance } = setSegmentValue(seg, base, nextBuffer, isPm);
|
|
13833
|
+
if (!advance && isUnfinishedBlocked?.(date))
|
|
13834
|
+
return;
|
|
13560
13835
|
applyDateFromSegment(date);
|
|
13561
13836
|
if (advance) {
|
|
13562
13837
|
typedDigitsRef.current = '';
|
|
@@ -13567,11 +13842,23 @@ function useSegmentedEntry(params) {
|
|
|
13567
13842
|
}
|
|
13568
13843
|
}
|
|
13569
13844
|
// ----- free-form / popover key handling -----
|
|
13570
|
-
|
|
13845
|
+
// With no segment to step, ArrowDown opens the popover with or without
|
|
13846
|
+
// Alt, unless the field is read-only or disabled, as focus and the
|
|
13847
|
+
// launcher don't open it then either. Alt+ArrowUp closes it.
|
|
13848
|
+
if (e.key === 'ArrowDown' &&
|
|
13849
|
+
!isOpen &&
|
|
13850
|
+
popover &&
|
|
13851
|
+
!disabled &&
|
|
13852
|
+
!readOnly) {
|
|
13571
13853
|
e.preventDefault();
|
|
13572
13854
|
setOpen(true);
|
|
13573
13855
|
return;
|
|
13574
13856
|
}
|
|
13857
|
+
if (e.key === 'ArrowUp' && e.altKey && isOpen) {
|
|
13858
|
+
e.preventDefault();
|
|
13859
|
+
setOpen(false);
|
|
13860
|
+
return;
|
|
13861
|
+
}
|
|
13575
13862
|
if (e.key === 'Escape' && isOpen) {
|
|
13576
13863
|
e.preventDefault();
|
|
13577
13864
|
setOpen(false);
|
|
@@ -13601,11 +13888,14 @@ function useSegmentedEntry(params) {
|
|
|
13601
13888
|
activeSegmentIdx,
|
|
13602
13889
|
segmentBaseDate,
|
|
13603
13890
|
applyDateFromSegment,
|
|
13891
|
+
isUnfinishedBlocked,
|
|
13604
13892
|
moveSegment,
|
|
13605
13893
|
isOpen,
|
|
13606
13894
|
setOpen,
|
|
13607
13895
|
closeOnSelect,
|
|
13608
13896
|
popover,
|
|
13897
|
+
disabled,
|
|
13898
|
+
readOnly,
|
|
13609
13899
|
isUntouched,
|
|
13610
13900
|
tryParse,
|
|
13611
13901
|
text,
|
|
@@ -13650,37 +13940,6 @@ function supportsInputType(type) {
|
|
|
13650
13940
|
return supported;
|
|
13651
13941
|
}
|
|
13652
13942
|
|
|
13653
|
-
const pad2$1 = (n) => String(n).padStart(2, '0');
|
|
13654
|
-
/**
|
|
13655
|
-
* The value as the native input and the hidden form input carry it:
|
|
13656
|
-
* `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
|
|
13657
|
-
* `<input type="month">` use. The year is padded to four digits, as those
|
|
13658
|
-
* inputs require and as the `YYYY` token displays it. HTML has no such shape
|
|
13659
|
-
* for a year before 1, so a date then is empty, as those inputs would make it.
|
|
13660
|
-
*/
|
|
13661
|
-
const toIsoValue = (d, granularity) => {
|
|
13662
|
-
if (d.getFullYear() < FIRST_YEAR)
|
|
13663
|
-
return '';
|
|
13664
|
-
const year = String(d.getFullYear()).padStart(4, '0');
|
|
13665
|
-
if (granularity === 'year')
|
|
13666
|
-
return year;
|
|
13667
|
-
const month = `${year}-${pad2$1(d.getMonth() + 1)}`;
|
|
13668
|
-
return granularity === 'month' ? month : `${month}-${pad2$1(d.getDate())}`;
|
|
13669
|
-
};
|
|
13670
|
-
/**
|
|
13671
|
-
* Read a native `type="date"` or `type="month"` value back into a Date. HTML
|
|
13672
|
-
* allows a year of four or more digits, as `toIsoValue` writes one past 9999.
|
|
13673
|
-
*/
|
|
13674
|
-
const fromIsoValue = (s, granularity) => {
|
|
13675
|
-
if (granularity === 'month') {
|
|
13676
|
-
const m = /^(\d{4,})-(\d{2})$/.exec(s);
|
|
13677
|
-
return m ? makeDate(Number(m[1]), Number(m[2]) - 1) : null;
|
|
13678
|
-
}
|
|
13679
|
-
const m = /^(\d{4,})-(\d{2})-(\d{2})$/.exec(s);
|
|
13680
|
-
if (!m)
|
|
13681
|
-
return null;
|
|
13682
|
-
return makeDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
13683
|
-
};
|
|
13684
13943
|
const DEFAULT_FORMATS = {
|
|
13685
13944
|
day: DEFAULT_DATE_FORMAT,
|
|
13686
13945
|
month: DEFAULT_MONTH_FORMAT,
|
|
@@ -13782,7 +14041,11 @@ const DateInputBase = forwardRef((props, ref) => {
|
|
|
13782
14041
|
// them, so the month or year containing `min` stays reachable.
|
|
13783
14042
|
const periodMin = useMemo(() => isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity), [lowerBound, isDayGranularity, granularity]);
|
|
13784
14043
|
const periodMax = useMemo(() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max), [max, isDayGranularity, granularity]);
|
|
13785
|
-
|
|
14044
|
+
// A pick is the day at midnight, as a click on its cell gives. Enter hands
|
|
14045
|
+
// over the focused date, which can carry a time of day from the clock, a
|
|
14046
|
+
// value or a bound, so its time is dropped here.
|
|
14047
|
+
const handleSelect = useCallback((picked) => {
|
|
14048
|
+
const d = startOfDay(picked);
|
|
13786
14049
|
if (!isWithin(d, periodMin, periodMax))
|
|
13787
14050
|
return;
|
|
13788
14051
|
commitValue(d);
|
|
@@ -13954,13 +14217,14 @@ const DateInput = forwardRef((props, ref) => {
|
|
|
13954
14217
|
// icon-left container has nothing to anchor to, and it renders no
|
|
13955
14218
|
// Control inside one or out. The left icon is checked as the caller
|
|
13956
14219
|
// passed it: the default glyph is this component's own choice, not a
|
|
13957
|
-
// prop the caller set.
|
|
14220
|
+
// prop the caller set, so it goes in only for the advice.
|
|
13958
14221
|
const ownControl = rendersOwnControl('DateInput', {
|
|
13959
14222
|
insideControl,
|
|
13960
14223
|
inline: baseProps.inline,
|
|
13961
14224
|
isLoading,
|
|
13962
14225
|
iconLeft,
|
|
13963
14226
|
iconLeftName: props.iconLeftName,
|
|
14227
|
+
defaultIconLeftName: iconLeftName,
|
|
13964
14228
|
iconLeftSize,
|
|
13965
14229
|
iconRight,
|
|
13966
14230
|
iconRightName,
|
|
@@ -14350,23 +14614,28 @@ const WheelInner = ({ values, index, onChange, formatLabel, formatAriaText, aria
|
|
|
14350
14614
|
cancelRaf();
|
|
14351
14615
|
commitPosition(Math.round(positionRef.current) + delta);
|
|
14352
14616
|
}, [disabled, cancelRaf, commitPosition]);
|
|
14617
|
+
// The keys are a spinbutton's: up raises the value and down lowers it. The
|
|
14618
|
+
// values run down the wheel, so up brings the one below the band into it,
|
|
14619
|
+
// the way a drag or a scroll that moves the items up does.
|
|
14353
14620
|
const onKeyDown = useCallback((e) => {
|
|
14621
|
+
if (isPopoverToggleKey(e))
|
|
14622
|
+
return;
|
|
14354
14623
|
switch (e.key) {
|
|
14355
14624
|
case 'ArrowUp':
|
|
14356
14625
|
e.preventDefault();
|
|
14357
|
-
moveBy(
|
|
14626
|
+
moveBy(1);
|
|
14358
14627
|
break;
|
|
14359
14628
|
case 'ArrowDown':
|
|
14360
14629
|
e.preventDefault();
|
|
14361
|
-
moveBy(1);
|
|
14630
|
+
moveBy(-1);
|
|
14362
14631
|
break;
|
|
14363
14632
|
case 'PageUp':
|
|
14364
14633
|
e.preventDefault();
|
|
14365
|
-
moveBy(
|
|
14634
|
+
moveBy(5);
|
|
14366
14635
|
break;
|
|
14367
14636
|
case 'PageDown':
|
|
14368
14637
|
e.preventDefault();
|
|
14369
|
-
moveBy(5);
|
|
14638
|
+
moveBy(-5);
|
|
14370
14639
|
break;
|
|
14371
14640
|
case 'Home':
|
|
14372
14641
|
e.preventDefault();
|
|
@@ -14683,6 +14952,7 @@ const TimeWheels = ({ value, onChange, hourFormat = '24', enableSeconds = false,
|
|
|
14683
14952
|
hours: h,
|
|
14684
14953
|
minutes: m,
|
|
14685
14954
|
seconds: s,
|
|
14955
|
+
milliseconds: 0,
|
|
14686
14956
|
});
|
|
14687
14957
|
return !unselectableTimes(probe);
|
|
14688
14958
|
}, [unselectableTimes]);
|
|
@@ -14949,9 +15219,16 @@ const TimeInputBase = forwardRef((props, ref) => {
|
|
|
14949
15219
|
setInternalValue(next);
|
|
14950
15220
|
onChange?.(next);
|
|
14951
15221
|
}, [isControlled, onChange]);
|
|
15222
|
+
// An empty field starts from today, at the whole minute or second the
|
|
15223
|
+
// wheels show.
|
|
14952
15224
|
const handleSpinnerChange = useCallback((parts) => {
|
|
14953
|
-
const
|
|
14954
|
-
|
|
15225
|
+
const next = value
|
|
15226
|
+
? setTimeOfDay(value, parts)
|
|
15227
|
+
: setTimeOfDay(new Date(), {
|
|
15228
|
+
...parts,
|
|
15229
|
+
seconds: parts.seconds ?? 0,
|
|
15230
|
+
milliseconds: 0,
|
|
15231
|
+
});
|
|
14955
15232
|
if (!isWithin(next, lowerBound, max))
|
|
14956
15233
|
return;
|
|
14957
15234
|
commitValue(next);
|
|
@@ -15082,13 +15359,14 @@ const TimeInput = forwardRef((props, ref) => {
|
|
|
15082
15359
|
// icon-left container has nothing to anchor to, and it renders no
|
|
15083
15360
|
// Control inside one or out. The left icon is checked as the caller
|
|
15084
15361
|
// passed it: the default glyph is this component's own choice, not a
|
|
15085
|
-
// prop the caller set.
|
|
15362
|
+
// prop the caller set, so it goes in only for the advice.
|
|
15086
15363
|
const ownControl = rendersOwnControl('TimeInput', {
|
|
15087
15364
|
insideControl,
|
|
15088
15365
|
inline: baseProps.inline,
|
|
15089
15366
|
isLoading,
|
|
15090
15367
|
iconLeft,
|
|
15091
15368
|
iconLeftName: props.iconLeftName,
|
|
15369
|
+
defaultIconLeftName: iconLeftName,
|
|
15092
15370
|
iconLeftSize,
|
|
15093
15371
|
iconRight,
|
|
15094
15372
|
iconRightName,
|
|
@@ -15266,6 +15544,34 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
15266
15544
|
if (!open)
|
|
15267
15545
|
setTimeOpen(false);
|
|
15268
15546
|
}, [open]);
|
|
15547
|
+
// The footer's Time button opens the wheels, and the hours wheel takes
|
|
15548
|
+
// focus so the keys turn it straight away. The calendar they cover is
|
|
15549
|
+
// inert meanwhile, so no tab stop, pointer or screen reader reaches a day
|
|
15550
|
+
// behind them; the attribute is set on the element, as React 18 has no
|
|
15551
|
+
// `inert` prop. Closing the wheels hands focus back to the button: focus
|
|
15552
|
+
// in them would fall to the page as they unmount, and a pointer press
|
|
15553
|
+
// outside them has already dropped it on the panel or the page. Focus the
|
|
15554
|
+
// keys moved on to the footer, such as Reset, stays there.
|
|
15555
|
+
const panelRef = useRef(null);
|
|
15556
|
+
const calendarRef = useRef(null);
|
|
15557
|
+
const timeButtonRef = useRef(null);
|
|
15558
|
+
const timeOverlayRef = useRef(null);
|
|
15559
|
+
useEffect(() => {
|
|
15560
|
+
calendarRef.current?.toggleAttribute('inert', timeOpen);
|
|
15561
|
+
if (!timeOpen)
|
|
15562
|
+
return;
|
|
15563
|
+
timeOverlayRef.current
|
|
15564
|
+
?.querySelector('[role="spinbutton"]')
|
|
15565
|
+
?.focus();
|
|
15566
|
+
}, [timeOpen]);
|
|
15567
|
+
const closeTime = useCallback(() => {
|
|
15568
|
+
const panel = panelRef.current;
|
|
15569
|
+
const active = getActiveElementInTree(panel);
|
|
15570
|
+
const movedOn = !!panel?.contains(active) && !timeOverlayRef.current?.contains(active);
|
|
15571
|
+
if (!movedOn)
|
|
15572
|
+
timeButtonRef.current?.focus();
|
|
15573
|
+
setTimeOpen(false);
|
|
15574
|
+
}, []);
|
|
15269
15575
|
const commitValue = useCallback((next) => {
|
|
15270
15576
|
if (!isControlled)
|
|
15271
15577
|
setInternalValue(next);
|
|
@@ -15274,20 +15580,32 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
15274
15580
|
setFocusedDate(next);
|
|
15275
15581
|
onChange?.(next);
|
|
15276
15582
|
}, [isControlled, onChange]);
|
|
15583
|
+
// A picked day takes the value's whole time of day, seconds and all, or
|
|
15584
|
+
// midnight in an empty field. The day handed over by the keyboard is the
|
|
15585
|
+
// calendar's focused date, which can carry the time of the clock it
|
|
15586
|
+
// started from, so none of its time is kept.
|
|
15277
15587
|
const handleDateSelect = useCallback((d) => {
|
|
15278
15588
|
const merged = setTimeOfDay(d, {
|
|
15279
15589
|
hours: value?.getHours() ?? 0,
|
|
15280
15590
|
minutes: value?.getMinutes() ?? 0,
|
|
15281
|
-
seconds:
|
|
15591
|
+
seconds: value?.getSeconds() ?? 0,
|
|
15592
|
+
milliseconds: value?.getMilliseconds() ?? 0,
|
|
15282
15593
|
});
|
|
15283
15594
|
if (!isWithin(merged, lowerBound, max))
|
|
15284
15595
|
return;
|
|
15285
15596
|
commitValue(merged);
|
|
15286
15597
|
setFocusedDate(d);
|
|
15287
|
-
}, [value,
|
|
15598
|
+
}, [value, lowerBound, max, commitValue]);
|
|
15599
|
+
// The wheels set the value's time. An empty field starts from the focused
|
|
15600
|
+
// day, at the whole minute or second the wheels show.
|
|
15288
15601
|
const handleTimeChange = useCallback((parts) => {
|
|
15289
|
-
const
|
|
15290
|
-
|
|
15602
|
+
const next = value
|
|
15603
|
+
? setTimeOfDay(value, parts)
|
|
15604
|
+
: setTimeOfDay(focusedDate, {
|
|
15605
|
+
...parts,
|
|
15606
|
+
seconds: parts.seconds ?? 0,
|
|
15607
|
+
milliseconds: 0,
|
|
15608
|
+
});
|
|
15291
15609
|
if (!isWithin(next, lowerBound, max))
|
|
15292
15610
|
return;
|
|
15293
15611
|
commitValue(next);
|
|
@@ -15301,8 +15619,12 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
15301
15619
|
const fmt = typeof defaultFormat === 'string' ? defaultFormat : undefined;
|
|
15302
15620
|
return parseDate(trimmed, fmt ?? DEFAULT_DATETIME_FORMAT);
|
|
15303
15621
|
}, [parse, defaultFormat, locale]);
|
|
15304
|
-
// The Date the user edits when starting without a current value
|
|
15305
|
-
|
|
15622
|
+
// The Date the user edits when starting without a current value: now, at
|
|
15623
|
+
// the whole minute, or the whole second with `enableSeconds`.
|
|
15624
|
+
const makeBaseDate = useCallback(() => setTimeOfDay(new Date(), {
|
|
15625
|
+
seconds: enableSeconds ? undefined : 0,
|
|
15626
|
+
milliseconds: 0,
|
|
15627
|
+
}), [enableSeconds]);
|
|
15306
15628
|
const inputReadOnlyAttr = !!readOnly || !editable;
|
|
15307
15629
|
const canOpen = !!popover && !disabled && !readOnly;
|
|
15308
15630
|
// Blocking predicate for manual entry, combining the calendar's
|
|
@@ -15366,20 +15688,20 @@ const DateTimeInputBase = forwardRef((props, ref) => {
|
|
|
15366
15688
|
minutes: value?.getMinutes() ?? 0,
|
|
15367
15689
|
seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
|
|
15368
15690
|
};
|
|
15369
|
-
const panel = (jsxs("div", { className: panelClass, onKeyDown: e => {
|
|
15691
|
+
const panel = (jsxs("div", { ref: panelRef, className: panelClass, onKeyDown: e => {
|
|
15370
15692
|
// First Escape collapses the floating time wheels; the popover's own
|
|
15371
15693
|
// Escape handler (which closes the whole panel) only sees the second.
|
|
15372
15694
|
if (timeOpen && e.key === 'Escape') {
|
|
15373
15695
|
e.stopPropagation();
|
|
15374
|
-
|
|
15696
|
+
closeTime();
|
|
15375
15697
|
}
|
|
15376
|
-
}, children: [jsxs("div", { className: calendarWrapClass, children: [jsx(Calendar, { value: value, focusedDate: focusedDate, onSelect: handleDateSelect, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size, id: `${popoverId}-cal`, autoFocusCell: open, labels: labels }), timeOpen && (jsx("div", { className: timeOverlayClass, onClick: e => {
|
|
15698
|
+
}, children: [jsxs("div", { className: calendarWrapClass, children: [jsx("div", { ref: calendarRef, children: jsx(Calendar, { value: value, focusedDate: focusedDate, onSelect: handleDateSelect, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size, id: `${popoverId}-cal`, autoFocusCell: open, labels: labels }) }), timeOpen && (jsx("div", { ref: timeOverlayRef, className: timeOverlayClass, onClick: e => {
|
|
15377
15699
|
// Tap outside the wheel card (on the covered calendar area)
|
|
15378
15700
|
// collapses the wheels without selecting a date, matching the
|
|
15379
15701
|
// native behavior.
|
|
15380
15702
|
if (e.target === e.currentTarget)
|
|
15381
|
-
|
|
15382
|
-
}, children: jsx("div", { className: timeCardClass, children: jsx(TimeWheels, { value: spinnerValue, onChange: handleTimeChange, hourFormat: effectiveHourFormat, enableSeconds: enableSeconds, incrementHours: incrementHours, incrementMinutes: incrementMinutes, incrementSeconds: incrementSeconds, unselectableTimes: unselectableTimes, color: color, size: size, disabled: disabled, id: `${popoverId}-time`, labels: labels, itemHeight: wheelItemHeight, audioTick: effectiveAudioTick, onCommit: () => setOpen(false) }) }) }))] }), jsxs("div", { className: footerClass, children: [jsxs("button", { type: "button", className: footerTimeClass, onClick: () =>
|
|
15703
|
+
closeTime();
|
|
15704
|
+
}, children: jsx("div", { className: timeCardClass, children: jsx(TimeWheels, { value: spinnerValue, onChange: handleTimeChange, hourFormat: effectiveHourFormat, enableSeconds: enableSeconds, incrementHours: incrementHours, incrementMinutes: incrementMinutes, incrementSeconds: incrementSeconds, unselectableTimes: unselectableTimes, color: color, size: size, disabled: disabled, id: `${popoverId}-time`, labels: labels, itemHeight: wheelItemHeight, audioTick: effectiveAudioTick, onCommit: () => setOpen(false) }) }) }))] }), jsxs("div", { className: footerClass, children: [jsxs("button", { ref: timeButtonRef, type: "button", className: footerTimeClass, onClick: () => (timeOpen ? closeTime() : setTimeOpen(true)), "aria-expanded": timeOpen, disabled: disabled, children: [jsx("span", { children: t.time }), jsx("span", { className: footerTimePillClass, children: value ? formatTime(value, timeDisplayFormat, locale) : '—' })] }), jsxs("div", { className: footerActionsClass, children: [jsx("button", { type: "button", className: footerResetClass, onClick: () => commitValue(valueAtOpenRef.current), children: t.reset }), jsx("button", { type: "button", className: footerDoneClass, "aria-label": t.done, onClick: () => setOpen(false), children: jsx("svg", { width: "22", height: "22", viewBox: "0 0 22 22", "aria-hidden": "true", children: jsx("path", { d: "M5 11l4 4 8-8", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] })] }));
|
|
15383
15705
|
if (inline) {
|
|
15384
15706
|
return (jsxs("div", { ...cleanRest, ref: containerRef, className: classNames(containerClass, bulmaHelperClasses, className), children: [panel, name && (jsx("input", { type: "hidden", name: name, form: form, value: value ? toIsoDateTime(value, enableSeconds) : '', required: required }))] }));
|
|
15385
15707
|
}
|
|
@@ -15423,13 +15745,14 @@ const DateTimeInput = forwardRef((props, ref) => {
|
|
|
15423
15745
|
// icon-left container has nothing to anchor to, and it renders no
|
|
15424
15746
|
// Control inside one or out. The left icon is checked as the caller
|
|
15425
15747
|
// passed it: the default glyph is this component's own choice, not a
|
|
15426
|
-
// prop the caller set.
|
|
15748
|
+
// prop the caller set, so it goes in only for the advice.
|
|
15427
15749
|
const ownControl = rendersOwnControl('DateTimeInput', {
|
|
15428
15750
|
insideControl,
|
|
15429
15751
|
inline: baseProps.inline,
|
|
15430
15752
|
isLoading,
|
|
15431
15753
|
iconLeft,
|
|
15432
15754
|
iconLeftName: props.iconLeftName,
|
|
15755
|
+
defaultIconLeftName: iconLeftName,
|
|
15433
15756
|
iconLeftSize,
|
|
15434
15757
|
iconRight,
|
|
15435
15758
|
iconRightName,
|
|
@@ -15464,6 +15787,354 @@ const DateTimeInput = forwardRef((props, ref) => {
|
|
|
15464
15787
|
});
|
|
15465
15788
|
DateTimeInput.displayName = 'DateTimeInput';
|
|
15466
15789
|
|
|
15790
|
+
const EMPTY_RANGE = [null, null];
|
|
15791
|
+
/** A range's end as the form and the native inputs carry it. */
|
|
15792
|
+
const isoDay = (d) => (d ? toIsoValue(d, 'day') : '');
|
|
15793
|
+
/** Today at local midnight, where typing into an empty end starts. */
|
|
15794
|
+
const today = () => startOfDay(new Date());
|
|
15795
|
+
/**
|
|
15796
|
+
* Raw DateRangeInput: the two inputs and the popover calendar without
|
|
15797
|
+
* Field/Control wrapping. Use `DateRangeInput` for the convenience wrapper.
|
|
15798
|
+
*
|
|
15799
|
+
* @function
|
|
15800
|
+
* @param {DateRangeInputBaseProps} props
|
|
15801
|
+
* @returns {JSX.Element}
|
|
15802
|
+
*/
|
|
15803
|
+
const DateRangeInputBase = forwardRef((props, ref) => {
|
|
15804
|
+
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 = true, position = 'bottom-left', appendToBody = false, color, size, isRounded, shouldDisableDate, unselectableDates, allowDisabledInRange = false, firstDayOfWeek = 0, dayNames, monthNames, nearbyMonthDays = true, iconLeftName: _iconLeftName, triggerIcon: triggerIconProp, triggerIconName = 'chevron-down', labels, name, startName, endName, form, required, id, className, onFocus, onFocusCapture, ...rest } = props;
|
|
15805
|
+
// The launcher gives way to the loading spinner of a Control this sits in.
|
|
15806
|
+
const controlLoading = useControlLoading();
|
|
15807
|
+
const triggerIcon = triggerIconProp ?? !controlLoading;
|
|
15808
|
+
const t = mergeLabels(labels);
|
|
15809
|
+
const resolvedFormat = format ?? DEFAULT_DATE_FORMAT;
|
|
15810
|
+
const isControlled = controlledValue !== undefined;
|
|
15811
|
+
const [internalValue, setInternalValue] = useState(defaultValue ?? EMPTY_RANGE);
|
|
15812
|
+
const range = isControlled ? controlledValue : internalValue;
|
|
15813
|
+
const [start, end] = range;
|
|
15814
|
+
// Nothing before year 1 is in range, as in HTML's date inputs.
|
|
15815
|
+
const lowerBound = useMemo(() => floorMin(min), [min]);
|
|
15816
|
+
const [focusedDate, setFocusedDate] = useState(() => clampDate(start ?? end ?? new Date(), lowerBound, max));
|
|
15817
|
+
// Re-clamp the focused day if the bounds change after mount, so the
|
|
15818
|
+
// calendar never shows a month outside them.
|
|
15819
|
+
useEffect(() => {
|
|
15820
|
+
setFocusedDate(prev => clampDate(prev, lowerBound, max));
|
|
15821
|
+
}, [lowerBound, max]);
|
|
15822
|
+
const [open, setOpenState] = useState(false);
|
|
15823
|
+
const [startText, setStartText] = useState(() => start ? formatDate(start, resolvedFormat, locale) : '');
|
|
15824
|
+
const [endText, setEndText] = useState(() => end ? formatDate(end, resolvedFormat, locale) : '');
|
|
15825
|
+
// Each input shows its end of the range as it changes from outside.
|
|
15826
|
+
useEffect(() => {
|
|
15827
|
+
setStartText(start ? formatDate(start, resolvedFormat, locale) : '');
|
|
15828
|
+
}, [start, resolvedFormat, locale]);
|
|
15829
|
+
useEffect(() => {
|
|
15830
|
+
setEndText(end ? formatDate(end, resolvedFormat, locale) : '');
|
|
15831
|
+
}, [end, resolvedFormat, locale]);
|
|
15832
|
+
const containerRef = useRef(null);
|
|
15833
|
+
const startRef = useRef(null);
|
|
15834
|
+
const endRef = useRef(null);
|
|
15835
|
+
const reactId = useId();
|
|
15836
|
+
const popoverId = id ? `${id}-popover` : `picker-${reactId}`;
|
|
15837
|
+
const { bulmaHelperClasses, rest: cleanRest } = useBulmaClasses(rest);
|
|
15838
|
+
const { shouldUseNative } = useNativeMobilePicker({
|
|
15839
|
+
force: mobileNative === 'auto' ? undefined : mobileNative,
|
|
15840
|
+
});
|
|
15841
|
+
const useNative = !inline && shouldUseNative;
|
|
15842
|
+
// The root borrows DateInput's container class, which places the launcher
|
|
15843
|
+
// and sizes a Control's icons around the field.
|
|
15844
|
+
const containerClass = usePrefixedClassNames('daterangeinput', 'dateinput-container');
|
|
15845
|
+
const fieldClass = usePrefixedClassNames('input', 'daterangeinput-field', {
|
|
15846
|
+
[`is-${color}`]: !!color,
|
|
15847
|
+
[`is-${size}`]: !!size,
|
|
15848
|
+
'is-rounded': isRounded,
|
|
15849
|
+
'is-disabled': disabled,
|
|
15850
|
+
});
|
|
15851
|
+
const inputClass = usePrefixedClassNames('daterangeinput-input');
|
|
15852
|
+
const separatorClass = usePrefixedClassNames('daterangeinput-separator');
|
|
15853
|
+
const triggerClass = usePrefixedClassNames('dateinput-trigger');
|
|
15854
|
+
// What the latest request asked for, so a second request in the same event
|
|
15855
|
+
// sees it before React renders. The callbacks fire here rather than in a
|
|
15856
|
+
// state updater, which StrictMode calls twice.
|
|
15857
|
+
const requestedOpenRef = useRef(false);
|
|
15858
|
+
const setOpen = useCallback((next) => {
|
|
15859
|
+
if (requestedOpenRef.current === next)
|
|
15860
|
+
return;
|
|
15861
|
+
requestedOpenRef.current = next;
|
|
15862
|
+
setOpenState(next);
|
|
15863
|
+
if (next)
|
|
15864
|
+
onOpen?.();
|
|
15865
|
+
else
|
|
15866
|
+
onClose?.();
|
|
15867
|
+
}, [onOpen, onClose]);
|
|
15868
|
+
// Focus that moves inside the field, from one input to the other or from
|
|
15869
|
+
// the launcher, is not focus arriving, so it opens nothing. Set as the
|
|
15870
|
+
// focus is captured on the way in, and cleared once the input has handled
|
|
15871
|
+
// it, so a click that follows still opens the popover.
|
|
15872
|
+
const focusFromInsideRef = useRef(false);
|
|
15873
|
+
const handleFocusCapture = (e) => {
|
|
15874
|
+
const from = e.relatedTarget;
|
|
15875
|
+
focusFromInsideRef.current = !!from && e.currentTarget.contains(from);
|
|
15876
|
+
onFocusCapture?.(e);
|
|
15877
|
+
};
|
|
15878
|
+
const handleFocus = (e) => {
|
|
15879
|
+
focusFromInsideRef.current = false;
|
|
15880
|
+
onFocus?.(e);
|
|
15881
|
+
};
|
|
15882
|
+
const requestOpen = useCallback((next) => {
|
|
15883
|
+
if (next && focusFromInsideRef.current)
|
|
15884
|
+
return;
|
|
15885
|
+
setOpen(next);
|
|
15886
|
+
}, [setOpen]);
|
|
15887
|
+
const commitRange = useCallback((next) => {
|
|
15888
|
+
if (!isControlled)
|
|
15889
|
+
setInternalValue(next);
|
|
15890
|
+
onChange?.(next);
|
|
15891
|
+
}, [isControlled, onChange]);
|
|
15892
|
+
const constraints = useMemo(() => ({ shouldDisableDate, unselectableDates }), [shouldDisableDate, unselectableDates]);
|
|
15893
|
+
// A day the calendar disables: shouldDisableDate or unselectableDates.
|
|
15894
|
+
// Typing and the native inputs check the bounds alongside it.
|
|
15895
|
+
const isBlockedDay = useCallback((d) => !!shouldDisableDate?.(d) ||
|
|
15896
|
+
!!unselectableDates?.some(u => isSameDay(u, d)), [shouldDisableDate, unselectableDates]);
|
|
15897
|
+
// An end the range can take: it is not disabled, and it closes a range
|
|
15898
|
+
// from the start, which it can't when it comes first or, unless allowed,
|
|
15899
|
+
// reaches over a disabled day.
|
|
15900
|
+
const isBlockedEnd = useCallback((d) => isBlockedDay(d) ||
|
|
15901
|
+
(!!start && !canCloseRange(start, d, constraints, allowDisabledInRange)), [isBlockedDay, start, constraints, allowDisabledInRange]);
|
|
15902
|
+
// A new start keeps the end only while the two still make a range, so a
|
|
15903
|
+
// start moved past the end, or over a disabled day before it, clears it.
|
|
15904
|
+
const commitStart = useCallback((next) => {
|
|
15905
|
+
const keepsEnd = !next ||
|
|
15906
|
+
!end ||
|
|
15907
|
+
canCloseRange(next, end, constraints, allowDisabledInRange);
|
|
15908
|
+
commitRange([next, keepsEnd ? end : null]);
|
|
15909
|
+
if (next)
|
|
15910
|
+
setFocusedDate(next);
|
|
15911
|
+
}, [end, constraints, allowDisabledInRange, commitRange]);
|
|
15912
|
+
// Every digit typed into the start commits, so a start passes through
|
|
15913
|
+
// values on its way to the one being typed. The first digits of a year make
|
|
15914
|
+
// a start centuries back, which can't keep the end once a disabled day may
|
|
15915
|
+
// lie between. Such a value waits for the segment's next digit rather than
|
|
15916
|
+
// clearing an end the finished start may keep.
|
|
15917
|
+
const startCostsEnd = useCallback((d) => !!end && !canCloseRange(d, end, constraints, allowDisabledInRange), [end, constraints, allowDisabledInRange]);
|
|
15918
|
+
const commitEnd = useCallback((next) => {
|
|
15919
|
+
commitRange([start, next]);
|
|
15920
|
+
if (next)
|
|
15921
|
+
setFocusedDate(next);
|
|
15922
|
+
}, [start, commitRange]);
|
|
15923
|
+
const handleRangeSelect = useCallback((from, to) => {
|
|
15924
|
+
commitRange([from, to]);
|
|
15925
|
+
setFocusedDate(to);
|
|
15926
|
+
if (closeOnSelect)
|
|
15927
|
+
setOpen(false);
|
|
15928
|
+
}, [commitRange, closeOnSelect, setOpen]);
|
|
15929
|
+
const tryParse = useCallback((s) => {
|
|
15930
|
+
const trimmed = s.trim();
|
|
15931
|
+
if (!trimmed)
|
|
15932
|
+
return null;
|
|
15933
|
+
const fmt = typeof format === 'string' ? format : undefined;
|
|
15934
|
+
return parse
|
|
15935
|
+
? parse(trimmed)
|
|
15936
|
+
: parseDate(trimmed, fmt ?? DEFAULT_DATE_FORMAT);
|
|
15937
|
+
}, [parse, format, locale]);
|
|
15938
|
+
// Typing into an empty end starts from the start, so only what differs
|
|
15939
|
+
// needs typing; an empty start starts from today.
|
|
15940
|
+
const makeEndBase = useCallback(() => (start ? startOfDay(start) : today()), [start]);
|
|
15941
|
+
// Where focus goes back as the popover closes: the input focused last,
|
|
15942
|
+
// which is the one that opened it unless the launcher did, or the start
|
|
15943
|
+
// input before either has had focus.
|
|
15944
|
+
const openerRef = useRef(null);
|
|
15945
|
+
const restoreFocusRef = useMemo(() => ({
|
|
15946
|
+
get current() {
|
|
15947
|
+
return openerRef.current ?? startRef.current;
|
|
15948
|
+
},
|
|
15949
|
+
}), []);
|
|
15950
|
+
const shared = {
|
|
15951
|
+
format: resolvedFormat,
|
|
15952
|
+
formatFn: formatDate,
|
|
15953
|
+
tryParse,
|
|
15954
|
+
locale,
|
|
15955
|
+
min: lowerBound,
|
|
15956
|
+
max,
|
|
15957
|
+
disabled,
|
|
15958
|
+
readOnly,
|
|
15959
|
+
editable,
|
|
15960
|
+
popover,
|
|
15961
|
+
openOnFocus,
|
|
15962
|
+
closeOnSelect,
|
|
15963
|
+
isOpen: open,
|
|
15964
|
+
setOpen: requestOpen,
|
|
15965
|
+
};
|
|
15966
|
+
// Each input is its own picker field. Its blur counts focus leaving it,
|
|
15967
|
+
// even for the other input, so text typed into one is read as focus moves
|
|
15968
|
+
// to the other.
|
|
15969
|
+
const { inputHandlers: startHandlers } = useSegmentedEntry({
|
|
15970
|
+
...shared,
|
|
15971
|
+
value: start,
|
|
15972
|
+
commitValue: commitStart,
|
|
15973
|
+
text: startText,
|
|
15974
|
+
setText: setStartText,
|
|
15975
|
+
makeBaseDate: today,
|
|
15976
|
+
isBlocked: isBlockedDay,
|
|
15977
|
+
isUnfinishedBlocked: startCostsEnd,
|
|
15978
|
+
inputRef: startRef,
|
|
15979
|
+
containerRef: startRef,
|
|
15980
|
+
onFocus: () => {
|
|
15981
|
+
openerRef.current = startRef.current;
|
|
15982
|
+
},
|
|
15983
|
+
});
|
|
15984
|
+
const { inputHandlers: endHandlers } = useSegmentedEntry({
|
|
15985
|
+
...shared,
|
|
15986
|
+
value: end,
|
|
15987
|
+
commitValue: commitEnd,
|
|
15988
|
+
text: endText,
|
|
15989
|
+
setText: setEndText,
|
|
15990
|
+
makeBaseDate: makeEndBase,
|
|
15991
|
+
isBlocked: isBlockedEnd,
|
|
15992
|
+
inputRef: endRef,
|
|
15993
|
+
containerRef: endRef,
|
|
15994
|
+
onFocus: () => {
|
|
15995
|
+
openerRef.current = endRef.current;
|
|
15996
|
+
},
|
|
15997
|
+
});
|
|
15998
|
+
const combinedRef = useCallback((node) => {
|
|
15999
|
+
startRef.current =
|
|
16000
|
+
node;
|
|
16001
|
+
if (typeof ref === 'function')
|
|
16002
|
+
ref(node);
|
|
16003
|
+
else if (ref)
|
|
16004
|
+
ref.current = node;
|
|
16005
|
+
}, [ref]);
|
|
16006
|
+
const handlePopoverClose = useCallback(() => setOpen(false), [setOpen]);
|
|
16007
|
+
const startField = startName ?? (name ? `${name}[start]` : undefined);
|
|
16008
|
+
const endField = endName ?? (name ? `${name}[end]` : undefined);
|
|
16009
|
+
const hiddenInputs = (jsxs(Fragment, { children: [startField && (jsx("input", { type: "hidden", name: startField, form: form, value: isoDay(start) })), endField && (jsx("input", { type: "hidden", name: endField, form: form, value: isoDay(end) }))] }));
|
|
16010
|
+
const separator = (jsx("span", { className: separatorClass, "aria-hidden": "true", children: t.rangeSeparator }));
|
|
16011
|
+
// Native mobile path: two <input type="date">s, held to the same rules as
|
|
16012
|
+
// typing. A change the rules refuse leaves the input showing the range.
|
|
16013
|
+
if (useNative) {
|
|
16014
|
+
const nativeChange = (commit, isBlocked) => (e) => {
|
|
16015
|
+
// An empty value is the end cleared. A date input gives nothing else
|
|
16016
|
+
// that doesn't read as a date.
|
|
16017
|
+
const parsed = fromIsoValue(e.target.value, 'day');
|
|
16018
|
+
if (!parsed)
|
|
16019
|
+
commit(null);
|
|
16020
|
+
else if (isWithin(parsed, lowerBound, max) && !isBlocked(parsed)) {
|
|
16021
|
+
commit(parsed);
|
|
16022
|
+
}
|
|
16023
|
+
};
|
|
16024
|
+
const minIso = min ? isoDay(lowerBound) : undefined;
|
|
16025
|
+
const maxIso = max ? isoDay(max) : undefined;
|
|
16026
|
+
return (jsxs("div", { ...cleanRest, ref: containerRef, role: "group", className: classNames(containerClass, bulmaHelperClasses, className), onFocus: onFocus, onFocusCapture: onFocusCapture, children: [jsxs("div", { className: fieldClass, children: [jsx("input", { ref: combinedRef, type: "date", id: id, "aria-label": t.rangeStart, className: inputClass, value: isoDay(start), onChange: nativeChange(commitStart, isBlockedDay), min: minIso, max: maxIso, disabled: disabled, readOnly: readOnly, required: required }), separator, jsx("input", { ref: endRef, type: "date", id: id ? `${id}-end` : undefined, "aria-label": t.rangeEnd, className: inputClass, value: isoDay(end), onChange: nativeChange(commitEnd, isBlockedEnd), min: start ? isoDay(start) : minIso, max: maxIso, disabled: disabled, readOnly: readOnly, required: required })] }), hiddenInputs] }));
|
|
16027
|
+
}
|
|
16028
|
+
const calendar = (jsx(Calendar, { range: range, onRangeSelect: handleRangeSelect, allowDisabledInRange: allowDisabledInRange, focusedDate: focusedDate, onFocusedDateChange: setFocusedDate, min: min, max: max, shouldDisableDate: shouldDisableDate, unselectableDates: unselectableDates, firstDayOfWeek: firstDayOfWeek, locale: locale, dayNames: dayNames, monthNames: monthNames, nearbyMonthDays: nearbyMonthDays, color: color, size: size,
|
|
16029
|
+
// The popover panel takes `popoverId`, so the calendar inside it takes
|
|
16030
|
+
// one of its own. Inline there is no panel.
|
|
16031
|
+
id: inline ? popoverId : `${popoverId}-cal`, autoFocusCell: open, labels: labels }));
|
|
16032
|
+
if (inline) {
|
|
16033
|
+
return (jsxs("div", { ...cleanRest, ref: containerRef, role: "group", className: classNames(containerClass, bulmaHelperClasses, className), onFocus: onFocus, onFocusCapture: onFocusCapture, children: [calendar, hiddenInputs] }));
|
|
16034
|
+
}
|
|
16035
|
+
const canOpen = !!popover && !disabled && !readOnly;
|
|
16036
|
+
const inputReadOnly = !!readOnly || !editable;
|
|
16037
|
+
// The field reads as one text box, so a press on it that misses both
|
|
16038
|
+
// inputs, on its padding, the gap or the separator, goes to the nearer
|
|
16039
|
+
// input and does what a press on that input does.
|
|
16040
|
+
const handleFieldMouseDown = (e) => {
|
|
16041
|
+
const startInput = startRef.current;
|
|
16042
|
+
const endInput = endRef.current;
|
|
16043
|
+
if (disabled || e.button !== 0)
|
|
16044
|
+
return;
|
|
16045
|
+
if (e.target === startInput || e.target === endInput)
|
|
16046
|
+
return;
|
|
16047
|
+
e.preventDefault();
|
|
16048
|
+
const middle = (startInput.getBoundingClientRect().right +
|
|
16049
|
+
endInput.getBoundingClientRect().left) /
|
|
16050
|
+
2;
|
|
16051
|
+
(e.clientX < middle ? startInput : endInput).focus();
|
|
16052
|
+
if (openOnFocus && canOpen)
|
|
16053
|
+
requestOpen(true);
|
|
16054
|
+
};
|
|
16055
|
+
const comboboxProps = {
|
|
16056
|
+
type: 'text',
|
|
16057
|
+
role: 'combobox',
|
|
16058
|
+
'aria-haspopup': 'dialog',
|
|
16059
|
+
'aria-expanded': open,
|
|
16060
|
+
'aria-controls': popoverId,
|
|
16061
|
+
autoComplete: 'off',
|
|
16062
|
+
className: inputClass,
|
|
16063
|
+
placeholder,
|
|
16064
|
+
disabled,
|
|
16065
|
+
readOnly: inputReadOnly,
|
|
16066
|
+
required,
|
|
16067
|
+
};
|
|
16068
|
+
return (jsxs("div", { ...cleanRest, ref: containerRef, role: "group", className: classNames(containerClass, bulmaHelperClasses, className), onFocus: handleFocus, onFocusCapture: handleFocusCapture, children: [jsxs("div", { className: fieldClass, onMouseDown: handleFieldMouseDown, children: [jsx("input", { ...comboboxProps, ref: combinedRef, id: id, "aria-label": t.rangeStart, value: startText, ...startHandlers }), separator, jsx("input", { ...comboboxProps, ref: endRef, id: id ? `${id}-end` : undefined, "aria-label": t.rangeEnd, value: endText, ...endHandlers })] }), popover && triggerIcon && (jsx("button", { type: "button", className: triggerClass,
|
|
16069
|
+
// Disabled whenever the popover can't open, so a click can.
|
|
16070
|
+
onClick: () => setOpen(!open), disabled: !canOpen, "aria-label": t.chooseDateRange, "aria-haspopup": "dialog", "aria-controls": popoverId, "aria-expanded": open, tabIndex: canOpen ? 0 : -1, children: jsx(Icon, { name: triggerIconName, size: size }) })), popover && (jsx(PickerPopover, { isOpen: open, onClose: handlePopoverClose, anchorRef: containerRef, position: position, appendToBody: appendToBody, ariaLabel: t.chooseDateRange, id: popoverId, restoreFocusRef: restoreFocusRef, initialFocusSelector: CALENDAR_FOCUSED_CELL, children: calendar })), hiddenInputs] }));
|
|
16071
|
+
});
|
|
16072
|
+
DateRangeInputBase.displayName = 'DateRangeInputBase';
|
|
16073
|
+
|
|
16074
|
+
/**
|
|
16075
|
+
* The `DateRangeInput` component is a form input for a start and end date, picked from one popover calendar or typed into two segmented inputs.
|
|
16076
|
+
*
|
|
16077
|
+
* @function
|
|
16078
|
+
* @param {DateRangeInputProps} props - Props for the DateRangeInput.
|
|
16079
|
+
* @returns {JSX.Element}
|
|
16080
|
+
*
|
|
16081
|
+
* @example
|
|
16082
|
+
* <DateRangeInput label="Stay" name="stay" min={new Date()} />
|
|
16083
|
+
*/
|
|
16084
|
+
const DateRangeInput = forwardRef((props, ref) => {
|
|
16085
|
+
const { label, labelSize, labelProps, horizontal, iconLeft, iconRight, iconLeftName = 'calendar', iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize, message, messageColor, fieldClassName, controlClassName, ...baseProps } = props;
|
|
16086
|
+
const insideField = useInsideField();
|
|
16087
|
+
const insideControl = useInsideControl();
|
|
16088
|
+
const ownField = rendersOwnField('DateRangeInput', {
|
|
16089
|
+
insideField,
|
|
16090
|
+
insideControl,
|
|
16091
|
+
label,
|
|
16092
|
+
message,
|
|
16093
|
+
horizontal,
|
|
16094
|
+
fieldClassName,
|
|
16095
|
+
});
|
|
16096
|
+
// Inline mode renders a bare calendar with no input, so the Control's
|
|
16097
|
+
// icon containers have nothing to anchor to, and it renders no Control
|
|
16098
|
+
// inside one or out. The left icon is checked as the caller passed it:
|
|
16099
|
+
// the default glyph is this component's own choice, not a prop the
|
|
16100
|
+
// caller set, so it goes in only for the advice.
|
|
16101
|
+
const ownControl = rendersOwnControl('DateRangeInput', {
|
|
16102
|
+
insideControl,
|
|
16103
|
+
inline: baseProps.inline,
|
|
16104
|
+
isLoading,
|
|
16105
|
+
iconLeft,
|
|
16106
|
+
iconLeftName: props.iconLeftName,
|
|
16107
|
+
defaultIconLeftName: iconLeftName,
|
|
16108
|
+
iconLeftSize,
|
|
16109
|
+
iconRight,
|
|
16110
|
+
iconRightName,
|
|
16111
|
+
iconRightSize,
|
|
16112
|
+
hasIconsLeft,
|
|
16113
|
+
hasIconsRight,
|
|
16114
|
+
isExpanded,
|
|
16115
|
+
controlSize,
|
|
16116
|
+
controlClassName,
|
|
16117
|
+
});
|
|
16118
|
+
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
16119
|
+
label,
|
|
16120
|
+
labelProps,
|
|
16121
|
+
rendersLabel: ownField,
|
|
16122
|
+
});
|
|
16123
|
+
const helpClass = usePrefixedClassNames('help', {
|
|
16124
|
+
[`is-${messageColor}`]: !!messageColor,
|
|
16125
|
+
});
|
|
16126
|
+
let content = (jsx(DateRangeInputBase, { ref: ref, "aria-labelledby": ariaLabelledBy, ...baseProps }));
|
|
16127
|
+
if (ownControl) {
|
|
16128
|
+
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 }));
|
|
16129
|
+
}
|
|
16130
|
+
const messageEl = message ? jsx("p", { className: helpClass, children: message }) : null;
|
|
16131
|
+
if (ownField) {
|
|
16132
|
+
return (jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
16133
|
+
}
|
|
16134
|
+
return (jsxs(Fragment, { children: [content, messageEl] }));
|
|
16135
|
+
});
|
|
16136
|
+
DateRangeInput.displayName = 'DateRangeInput';
|
|
16137
|
+
|
|
15467
16138
|
/**
|
|
15468
16139
|
* The `Cell` component provides a single Bulma grid cell for use inside the [`Grid`](./grid.md) component.
|
|
15469
16140
|
*
|
|
@@ -16843,5 +17514,5 @@ const Section = ({ size, className, children, color, bgColor, textColor, style,
|
|
|
16843
17514
|
return (jsx("section", { className: sectionClasses, style: mergeBulmaStyles(bulmaHelperStyles, style), ...rest, children: children }));
|
|
16844
17515
|
};
|
|
16845
17516
|
|
|
16846
|
-
export { Autocomplete, Avatar, Avatars, Badge, Block, Box, Breadcrumb, Button, Buttons, Card, Carousel, CarouselItem, Cell, Checkbox, Checkboxes, ClientOnly, Code, Collapse, Collapses, Column, Columns, ConfigProvider, Container, Content, Control, DateInput, DateInputBase, DateTimeInput, DateTimeInputBase, Delete, Dialog, DialogContainer, Divider, Dropdown, DropdownDivider, DropdownItem, Emphasis, Field, FieldBody, FieldLabel, Figure, File, Footer, Grid, Hero, HeroBody, HeroFoot, HeroHead, Icon, IconText, Image, Input, InputBase, Level, LevelItem, LevelLeft, LevelRight, Link, LinkButton, ListItem, Loader, Loading, Media, MediaContent, MediaLeft, MediaRight, Menu, MenuItem, MenuLabel, MenuList, Message, Modal, Navbar, NavbarBrand, NavbarBurger, NavbarDivider, NavbarDropdown, NavbarDropdownMenu, NavbarEnd, NavbarItem, NavbarLink, NavbarMenu, NavbarStart, Notification, NotificationContainer, NumberInput, Numberinput, OrderedList, Pagination, PaginationEllipsis, PaginationLink, PaginationList, PaginationNext, PaginationPrevious, Panel, PanelBlock, PanelButtonBlock, PanelCheckboxBlock, PanelHeading, PanelIcon, PanelInputBlock, PanelTabs, Paragraph, Popover, PopoverBody, PopoverClose, PopoverFooter, PopoverHeader, Portal, Pre, Progress, Radio, Radios, Rate, Reveal, Section, Select, SelectBase, Sidebar, Skeleton, Slider, Span, Step, Steps, Strong, SubTitle, Switch, Tab, TabContentItem, TabItem, TabList, Table, Tabs, TabsContent, Tag, TagInput, Taginput, Tags, Tbody, Td, TextArea, TextAreaBase, Tfoot, Th, Thead, Theme, TimeInput, TimeInputBase, Title, Toast, ToastContainer, Tooltip, Tr, UnorderedList, checkboxColors, checkboxSizes, classNames, createPrefixedClassNames, dialog, isBrowser$1 as isBrowser, mergeBulmaStyles, notification, prefixedClassNames, radioColors, radioSizes, switchColors, switchSizes, toast, useBulmaClasses, useClassPrefix, useColorClasses, useColorStyles, useConfig, useFlexboxClasses, useFocusTrap, useIconLibrary, useInsideControl, useInsideField, useIsHydrated, useOtherClasses, usePrefixedClass, usePrefixedClassNames, useSpacingClasses, useTypographyClasses, useVisibilityClasses, validAlignContents, validAlignItems, validAlignSelfs, validAlignments$1 as validAlignments, validAspectRatios, validAxisOverflows, validColorShades, validColors, validCursors, validDisplays, validFlexDirections, validFlexGrowShrink, validFlexWraps, validFloats, validFontFamilies, validGaps, validInteractions, validJustifyContents, validOverflows, validPositions, validRadii, validResponsives, validSchemeColors, validShadows, validSizes$1 as validSizes, validTableColors, validTextSizes, validTextTransforms, validTextWeights, validViewports, validVisibilities };
|
|
17517
|
+
export { Autocomplete, Avatar, Avatars, Badge, Block, Box, Breadcrumb, Button, Buttons, Card, Carousel, CarouselItem, Cell, Checkbox, Checkboxes, ClientOnly, Code, Collapse, Collapses, Column, Columns, ConfigProvider, Container, Content, Control, DateInput, DateInputBase, DateRangeInput, DateRangeInputBase, DateTimeInput, DateTimeInputBase, Delete, Dialog, DialogContainer, Divider, Dropdown, DropdownDivider, DropdownItem, Emphasis, Field, FieldBody, FieldLabel, Figure, File, Footer, Grid, Hero, HeroBody, HeroFoot, HeroHead, Icon, IconText, Image, Input, InputBase, Level, LevelItem, LevelLeft, LevelRight, Link, LinkButton, ListItem, Loader, Loading, Media, MediaContent, MediaLeft, MediaRight, Menu, MenuItem, MenuLabel, MenuList, Message, Modal, Navbar, NavbarBrand, NavbarBurger, NavbarDivider, NavbarDropdown, NavbarDropdownMenu, NavbarEnd, NavbarItem, NavbarLink, NavbarMenu, NavbarStart, Notification, NotificationContainer, NumberInput, Numberinput, OrderedList, Pagination, PaginationEllipsis, PaginationLink, PaginationList, PaginationNext, PaginationPrevious, Panel, PanelBlock, PanelButtonBlock, PanelCheckboxBlock, PanelHeading, PanelIcon, PanelInputBlock, PanelTabs, Paragraph, Popover, PopoverBody, PopoverClose, PopoverFooter, PopoverHeader, Portal, Pre, Progress, Radio, Radios, Rate, Reveal, Section, Select, SelectBase, Sidebar, Skeleton, Slider, Span, Step, Steps, Strong, SubTitle, Switch, Tab, TabContentItem, TabItem, TabList, Table, Tabs, TabsContent, Tag, TagInput, Taginput, Tags, Tbody, Td, TextArea, TextAreaBase, Tfoot, Th, Thead, Theme, TimeInput, TimeInputBase, Title, Toast, ToastContainer, Tooltip, Tr, UnorderedList, checkboxColors, checkboxSizes, classNames, createPrefixedClassNames, dialog, isBrowser$1 as isBrowser, mergeBulmaStyles, notification, prefixedClassNames, radioColors, radioSizes, switchColors, switchSizes, toast, useBulmaClasses, useClassPrefix, useColorClasses, useColorStyles, useConfig, useFlexboxClasses, useFocusTrap, useIconLibrary, useInsideControl, useInsideField, useIsHydrated, useOtherClasses, usePrefixedClass, usePrefixedClassNames, useSpacingClasses, useTypographyClasses, useVisibilityClasses, validAlignContents, validAlignItems, validAlignSelfs, validAlignments$1 as validAlignments, validAspectRatios, validAxisOverflows, validColorShades, validColors, validCursors, validDisplays, validFlexDirections, validFlexGrowShrink, validFlexWraps, validFloats, validFontFamilies, validGaps, validInteractions, validJustifyContents, validOverflows, validPositions, validRadii, validResponsives, validSchemeColors, validShadows, validSizes$1 as validSizes, validTableColors, validTextSizes, validTextTransforms, validTextWeights, validViewports, validVisibilities };
|
|
16847
17518
|
//# sourceMappingURL=index.esm.js.map
|