@allxsmith/bestax-bulma 5.40.0 → 5.42.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/esm/components/Carousel.js +282 -103
- package/dist/esm/components/Carousel.js.map +1 -1
- package/dist/esm/components/Menu.js +77 -2
- package/dist/esm/components/Menu.js.map +1 -1
- package/dist/esm/components/Modal.js +3 -3
- package/dist/esm/components/Modal.js.map +1 -1
- package/dist/esm/components/Popover.js +2 -2
- package/dist/esm/components/Popover.js.map +1 -1
- package/dist/esm/components/Sidebar.js +2 -2
- package/dist/esm/components/Sidebar.js.map +1 -1
- package/dist/esm/elements/Image.js +4 -12
- package/dist/esm/elements/Image.js.map +1 -1
- package/dist/esm/helpers/useFocusTrap.js +60 -41
- package/dist/esm/helpers/useFocusTrap.js.map +1 -1
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +442 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/types/components/Carousel.d.ts +96 -8
- package/dist/types/components/Menu.d.ts +49 -0
- package/dist/types/elements/Image.d.ts +6 -1
- package/dist/types/helpers/useFocusTrap.d.ts +16 -8
- package/dist/types-cjs/components/Carousel.d.ts +96 -8
- package/dist/types-cjs/components/Menu.d.ts +49 -0
- package/dist/types-cjs/elements/Image.d.ts +6 -1
- package/dist/types-cjs/helpers/useFocusTrap.d.ts +16 -8
- 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 +6 -6
- package/src/scss/components/_carousel.scss +77 -24
- package/src/scss/components/_menu.scss +130 -3
package/dist/index.cjs
CHANGED
|
@@ -3593,6 +3593,28 @@ const EmptyState = withSubComponents(EmptyStateRoot, {
|
|
|
3593
3593
|
Actions: EmptyStateActions,
|
|
3594
3594
|
}, 'EmptyState');
|
|
3595
3595
|
|
|
3596
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
3597
|
+
/** The major version of the React this bundle is running against. */
|
|
3598
|
+
const runningMajor = () => Number(React.version.split('.')[0]);
|
|
3599
|
+
/**
|
|
3600
|
+
* The prop that makes an element inert, spelled for the React in use, or
|
|
3601
|
+
* nothing when `inert` is false.
|
|
3602
|
+
*
|
|
3603
|
+
* Both majors must render `inert=""`, and each warns on the other's spelling
|
|
3604
|
+
* and then drops the attribute. React 19 treats `inert` as a boolean and
|
|
3605
|
+
* rejects the empty string. React 18 does not know the attribute, so it
|
|
3606
|
+
* passes a string through and rejects `true`.
|
|
3607
|
+
*/
|
|
3608
|
+
const inertProps = (inert, major = runningMajor()) => {
|
|
3609
|
+
if (!inert)
|
|
3610
|
+
return {};
|
|
3611
|
+
// The cast is for React 18's spelling: this package's types are React 19's,
|
|
3612
|
+
// where `inert` is a boolean.
|
|
3613
|
+
return (major >= 19
|
|
3614
|
+
? { inert: true }
|
|
3615
|
+
: { inert: '' });
|
|
3616
|
+
};
|
|
3617
|
+
|
|
3596
3618
|
// Context to track MenuList nesting level
|
|
3597
3619
|
const MenuListLevelContext = React.createContext(0);
|
|
3598
3620
|
/**
|
|
@@ -3702,11 +3724,83 @@ const MenuItem = React.forwardRef(function MenuItem(itemProps, ref) {
|
|
|
3702
3724
|
: {}), children: labelChildren }), nestedMenuLists] }));
|
|
3703
3725
|
});
|
|
3704
3726
|
MenuItem.displayName = 'MenuItem';
|
|
3727
|
+
/**
|
|
3728
|
+
* A section of the menu that opens and closes: its heading is a button drawn like `Menu.Label`, and its `Menu.List` of links shows while it's open.
|
|
3729
|
+
*
|
|
3730
|
+
* It's APG's disclosure navigation, for a sidebar of link groups: the links
|
|
3731
|
+
* stay ordinary links, each its own tab stop, and a closed section's list is
|
|
3732
|
+
* `hidden` and `inert`, so Tab and assistive tech skip it. It goes directly in
|
|
3733
|
+
* `Menu`, holding a `Menu.List`. Closing it while focus is on one of its links
|
|
3734
|
+
* moves focus to its heading.
|
|
3735
|
+
*
|
|
3736
|
+
* @function
|
|
3737
|
+
* @param {MenuSectionProps} props - Props for the MenuSection component.
|
|
3738
|
+
* @returns {JSX.Element} The rendered menu section.
|
|
3739
|
+
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/ | APG Disclosure Navigation Menu example}
|
|
3740
|
+
*
|
|
3741
|
+
* @example
|
|
3742
|
+
* <Menu>
|
|
3743
|
+
* <Menu.Section label="Components" defaultOpen>
|
|
3744
|
+
* <Menu.List>
|
|
3745
|
+
* <Menu.Item href="/button" active>Button</Menu.Item>
|
|
3746
|
+
* <Menu.Item href="/card">Card</Menu.Item>
|
|
3747
|
+
* </Menu.List>
|
|
3748
|
+
* </Menu.Section>
|
|
3749
|
+
* </Menu>
|
|
3750
|
+
*/
|
|
3751
|
+
const MenuSection = ({ className, label, children, open: controlledOpen, defaultOpen = false, onOpenChange, ...props }) => {
|
|
3752
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
3753
|
+
const sectionClass = usePrefixedClassNames('menu-section');
|
|
3754
|
+
const toggleClass = usePrefixedClassNames('menu-section-toggle');
|
|
3755
|
+
const labelClass = usePrefixedClassNames('menu-section-label');
|
|
3756
|
+
const chevronClass = usePrefixedClassNames('menu-section-chevron');
|
|
3757
|
+
const listId = React.useId();
|
|
3758
|
+
const toggleRef = React.useRef(null);
|
|
3759
|
+
const listRef = React.useRef(null);
|
|
3760
|
+
// The caller owns the state when `open` is set, and the section does
|
|
3761
|
+
// otherwise, as with Collapse. The button reports in both modes.
|
|
3762
|
+
const isControlled = controlledOpen !== undefined;
|
|
3763
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
|
|
3764
|
+
const isOpen = controlledOpen ?? internalOpen;
|
|
3765
|
+
const handleClick = () => {
|
|
3766
|
+
const next = !isOpen;
|
|
3767
|
+
onOpenChange?.(next);
|
|
3768
|
+
if (!isControlled)
|
|
3769
|
+
setInternalOpen(next);
|
|
3770
|
+
};
|
|
3771
|
+
// An app holding `open` can close a section while focus is on one of its
|
|
3772
|
+
// links, as a sidebar that keeps only the current route's section open
|
|
3773
|
+
// does after a link is followed. The browser then drops focus to <body>
|
|
3774
|
+
// a frame later, and the next Tab starts from the top of the page. The
|
|
3775
|
+
// heading is where the reader was, so focus goes there. A layout effect,
|
|
3776
|
+
// because it runs in the same task as the commit that hid the list, while
|
|
3777
|
+
// the link still has focus. A close from the button itself finds focus on
|
|
3778
|
+
// the button and leaves it.
|
|
3779
|
+
useClientLayoutEffect(() => {
|
|
3780
|
+
if (isOpen)
|
|
3781
|
+
return;
|
|
3782
|
+
const list = listRef.current;
|
|
3783
|
+
if (list.contains(getActiveElementInTree(list))) {
|
|
3784
|
+
toggleRef.current.focus();
|
|
3785
|
+
}
|
|
3786
|
+
}, [isOpen]);
|
|
3787
|
+
// A real button, so Enter and Space work with no key handler, and
|
|
3788
|
+
// `type="button"`, so a menu inside a form isn't submitted by it.
|
|
3789
|
+
//
|
|
3790
|
+
// A closed list is `hidden`, so it isn't drawn and find-in-page doesn't
|
|
3791
|
+
// reach it, and `inert`, so its links stay out of Tab and assistive tech
|
|
3792
|
+
// even if a stylesheet sets `display` on it and shows it anyway. The markup
|
|
3793
|
+
// is the same from the server and on both React majors, which `inertProps`
|
|
3794
|
+
// sees to. It stays mounted, so `aria-controls` always names an element
|
|
3795
|
+
// that exists.
|
|
3796
|
+
return (jsxRuntime.jsxs("div", { className: classNames(sectionClass, bulmaHelperClasses, className), ...rest, children: [jsxRuntime.jsxs("button", { ref: toggleRef, type: "button", className: toggleClass, "aria-expanded": isOpen, "aria-controls": listId, onClick: handleClick, children: [jsxRuntime.jsx("span", { className: labelClass, children: label }), jsxRuntime.jsx("span", { className: chevronClass, "aria-hidden": "true" })] }), jsxRuntime.jsx("div", { ref: listRef, id: listId, hidden: !isOpen, ...inertProps(!isOpen), children: children })] }));
|
|
3797
|
+
};
|
|
3705
3798
|
// Attach static subcomponents
|
|
3706
3799
|
const Menu = withSubComponents(MenuComponent, {
|
|
3707
3800
|
Label: MenuLabel,
|
|
3708
3801
|
List: MenuList,
|
|
3709
3802
|
Item: MenuItem,
|
|
3803
|
+
Section: MenuSection,
|
|
3710
3804
|
}, 'Menu');
|
|
3711
3805
|
|
|
3712
3806
|
const validMessageSizes = ['small', 'medium', 'large'];
|
|
@@ -4004,7 +4098,7 @@ const isDetailsSummary = (el) => el.parentElement?.localName === 'details' &&
|
|
|
4004
4098
|
* can read `-1` for an editable region or an `<embed>` that Tab still visits,
|
|
4005
4099
|
* and `0` for a `<video>` without controls that it doesn't. So a valid
|
|
4006
4100
|
* `tabindex` attribute decides, and otherwise the element's kind does.
|
|
4007
|
-
* Internal like `
|
|
4101
|
+
* Internal like `focusTabStop`: `src/index.ts` names what it takes from here.
|
|
4008
4102
|
*/
|
|
4009
4103
|
function isTabbableKind(el) {
|
|
4010
4104
|
const tabindex = el.getAttribute('tabindex');
|
|
@@ -4237,31 +4331,25 @@ function findTabStops(container) {
|
|
|
4237
4331
|
return stop;
|
|
4238
4332
|
};
|
|
4239
4333
|
const isStop = (el) => usable(el) && (!isGroupedRadio(el) || groupStop(el) === el);
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
break;
|
|
4334
|
+
// Checked only as they're reached, and the checks are kept, so finding
|
|
4335
|
+
// both ends costs what it did when only the ends were looked for.
|
|
4336
|
+
function* from(fromLast) {
|
|
4337
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
4338
|
+
const el = candidates[fromLast ? candidates.length - 1 - i : i];
|
|
4339
|
+
if (isStop(el))
|
|
4340
|
+
yield el;
|
|
4248
4341
|
}
|
|
4249
4342
|
}
|
|
4343
|
+
const first = from(false).next().value;
|
|
4344
|
+
if (!first)
|
|
4345
|
+
return null;
|
|
4250
4346
|
return {
|
|
4251
4347
|
first,
|
|
4252
|
-
last,
|
|
4348
|
+
last: from(true).next().value,
|
|
4253
4349
|
stopFor: node => (isGroupedRadio(node) && groupStop(node)) || node,
|
|
4350
|
+
from,
|
|
4254
4351
|
};
|
|
4255
4352
|
}
|
|
4256
|
-
/**
|
|
4257
|
-
* The first tab stop inside `container`, found the way the trap finds it, or
|
|
4258
|
-
* `null` when there is none. For a component that moves focus into a panel
|
|
4259
|
-
* itself rather than through the hook. `src/index.ts` names its exports from
|
|
4260
|
-
* this module, so this one stays internal.
|
|
4261
|
-
*/
|
|
4262
|
-
function firstTabStop(container) {
|
|
4263
|
-
return findTabStops(container)?.first ?? null;
|
|
4264
|
-
}
|
|
4265
4353
|
/**
|
|
4266
4354
|
* The element that has focus in the container's own document (an iframe's,
|
|
4267
4355
|
* when it renders into one), inside open shadow roots too. Focus inside a
|
|
@@ -4271,24 +4359,47 @@ function firstTabStop(container) {
|
|
|
4271
4359
|
* was inside a shadow root that was removed, so this falls back for it.
|
|
4272
4360
|
*/
|
|
4273
4361
|
const focusedElement = (doc) => getDeepestActiveElement(doc) ?? doc.body;
|
|
4362
|
+
/**
|
|
4363
|
+
* Moves focus to the first tab stop inside `container`, or the last when
|
|
4364
|
+
* `fromLast`, and to the container when there is none. Every move a trap
|
|
4365
|
+
* makes goes through here: into the container as it opens, to the other end
|
|
4366
|
+
* on a wrap, and back in after a Tab the browser carried out.
|
|
4367
|
+
*
|
|
4368
|
+
* A stop that doesn't take focus is passed over for the next one in. An
|
|
4369
|
+
* `<object>` or `<embed>` with nothing to show is one: it counts as a stop by
|
|
4370
|
+
* its kind, but browsers refuse to focus it, and which ones they refuse
|
|
4371
|
+
* depends on the browser and on what has loaded, so only trying tells. A
|
|
4372
|
+
* stop took focus when focus moved, wherever it went (a stop can hand focus
|
|
4373
|
+
* on), or when focus was on it already. `src/index.ts` names its exports from
|
|
4374
|
+
* this module, so this one stays internal.
|
|
4375
|
+
*/
|
|
4376
|
+
function focusTabStop(container, fromLast = false) {
|
|
4377
|
+
const doc = container.ownerDocument;
|
|
4378
|
+
for (const stop of findTabStops(container)?.from(fromLast) ?? []) {
|
|
4379
|
+
const before = focusedElement(doc);
|
|
4380
|
+
stop.focus();
|
|
4381
|
+
const now = focusedElement(doc);
|
|
4382
|
+
if (now !== before || inside(stop, now))
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
container.focus();
|
|
4386
|
+
}
|
|
4274
4387
|
/**
|
|
4275
4388
|
* Handles a Tab for a trap on `container`. When the browser has no tab stop
|
|
4276
4389
|
* left in that direction inside the container, focus goes to the stop at the
|
|
4277
4390
|
* other end and the key is marked used, and with no stops at all the
|
|
4278
4391
|
* container takes focus. Focus outside the container goes in at that end
|
|
4279
|
-
* too.
|
|
4280
|
-
* to should that Tab take it out anyway.
|
|
4392
|
+
* too. Returns whether it moved focus; otherwise the browser does.
|
|
4281
4393
|
*/
|
|
4282
4394
|
function wrapTab(e, container) {
|
|
4283
4395
|
const stops = findTabStops(container);
|
|
4284
4396
|
if (!stops) {
|
|
4285
4397
|
e.preventDefault();
|
|
4286
4398
|
container.focus();
|
|
4287
|
-
return
|
|
4399
|
+
return true;
|
|
4288
4400
|
}
|
|
4289
4401
|
const focused = focusedElement(container.ownerDocument);
|
|
4290
4402
|
const current = stops.stopFor(focused);
|
|
4291
|
-
const end = e.shiftKey ? stops.last : stops.first;
|
|
4292
4403
|
// Wrap whenever the browser has no stop left in that direction inside the
|
|
4293
4404
|
// container. Comparing positions rather than testing `=== first` also
|
|
4294
4405
|
// covers focus resting on the container or on an element outside the tab
|
|
@@ -4298,10 +4409,10 @@ function wrapTab(e, container) {
|
|
|
4298
4409
|
? !follows(container, stops.first, current)
|
|
4299
4410
|
: !follows(container, current, stops.last))) {
|
|
4300
4411
|
e.preventDefault();
|
|
4301
|
-
|
|
4302
|
-
return
|
|
4412
|
+
focusTabStop(container, e.shiftKey);
|
|
4413
|
+
return true;
|
|
4303
4414
|
}
|
|
4304
|
-
return
|
|
4415
|
+
return false;
|
|
4305
4416
|
}
|
|
4306
4417
|
/**
|
|
4307
4418
|
* Holds Tab inside the element `container` returns until the returned
|
|
@@ -4313,7 +4424,7 @@ function wrapTab(e, container) {
|
|
|
4313
4424
|
* overlay whose nodes are remounted while it's open, as moving to another
|
|
4314
4425
|
* portal target does, stays trapped without attaching again. `holds` is asked
|
|
4315
4426
|
* on every Tab, and the trap leaves the key alone while it says no. Internal
|
|
4316
|
-
* like `
|
|
4427
|
+
* like `focusTabStop`. It's for an overlay that holds Tab from a listener of
|
|
4317
4428
|
* its own on `window` and asks the overlay stack first, so it stands aside
|
|
4318
4429
|
* while another overlay is open over it, which a trap listening on the
|
|
4319
4430
|
* container can't do.
|
|
@@ -4324,9 +4435,10 @@ function trapTab(container, keyTarget, holds = () => true) {
|
|
|
4324
4435
|
// A window and an element type their listeners differently, so the trap
|
|
4325
4436
|
// listens through the base type both share.
|
|
4326
4437
|
const keys = keyTarget;
|
|
4327
|
-
//
|
|
4328
|
-
// held until that Tab has finished
|
|
4329
|
-
|
|
4438
|
+
// The end focus goes back to if a Tab the trap let the browser handle
|
|
4439
|
+
// takes it out (the last for Shift+Tab), held until that Tab has finished
|
|
4440
|
+
// moving focus, and `null` otherwise.
|
|
4441
|
+
let wrapFromLast = null;
|
|
4330
4442
|
let wrapTimer;
|
|
4331
4443
|
// `keyTarget` is any target, so the listener takes the base event type.
|
|
4332
4444
|
const handleKeyDown = (event) => {
|
|
@@ -4335,24 +4447,25 @@ function trapTab(container, keyTarget, holds = () => true) {
|
|
|
4335
4447
|
// A trap nested inside this one has already wrapped focus.
|
|
4336
4448
|
if (e.key !== 'Tab' || e.defaultPrevented || !box || !holds())
|
|
4337
4449
|
return;
|
|
4338
|
-
|
|
4339
|
-
if (!end)
|
|
4450
|
+
if (wrapTab(e, box))
|
|
4340
4451
|
return;
|
|
4341
|
-
|
|
4452
|
+
wrapFromLast = e.shiftKey;
|
|
4342
4453
|
clearTimeout(wrapTimer);
|
|
4343
4454
|
wrapTimer = setTimeout(() => {
|
|
4344
|
-
|
|
4455
|
+
wrapFromLast = null;
|
|
4345
4456
|
});
|
|
4346
4457
|
};
|
|
4347
4458
|
// The browser moves focus as soon as the keydown is over, so focus that
|
|
4348
|
-
// lands outside while `
|
|
4459
|
+
// lands outside while `wrapFromLast` is set got there by that Tab.
|
|
4349
4460
|
const handleFocusIn = (e) => {
|
|
4350
4461
|
const box = container();
|
|
4351
|
-
if (
|
|
4462
|
+
if (wrapFromLast === null || !box)
|
|
4352
4463
|
return;
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4464
|
+
if (inside(box, e.composedPath()[0]))
|
|
4465
|
+
return;
|
|
4466
|
+
const fromLast = wrapFromLast;
|
|
4467
|
+
wrapFromLast = null;
|
|
4468
|
+
focusTabStop(box, fromLast);
|
|
4356
4469
|
};
|
|
4357
4470
|
keys.addEventListener('keydown', handleKeyDown);
|
|
4358
4471
|
doc.addEventListener('focusin', handleFocusIn, true);
|
|
@@ -4438,7 +4551,7 @@ function useFocusTrap(containerRef, options = {}) {
|
|
|
4438
4551
|
const inTrap = initial && inside(container, initial) ? initial : null;
|
|
4439
4552
|
inTrap?.focus();
|
|
4440
4553
|
if (!inTrap || !inside(container, focusedElement(doc))) {
|
|
4441
|
-
(
|
|
4554
|
+
focusTabStop(container);
|
|
4442
4555
|
}
|
|
4443
4556
|
const releaseTab = trapTab(() => container, container);
|
|
4444
4557
|
return () => {
|
|
@@ -4727,8 +4840,8 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
|
|
|
4727
4840
|
previouslyFocusedRef.current =
|
|
4728
4841
|
getDeepestActiveElement();
|
|
4729
4842
|
const node = modalRootRef.current;
|
|
4730
|
-
|
|
4731
|
-
|
|
4843
|
+
if (node)
|
|
4844
|
+
focusTabStop(node);
|
|
4732
4845
|
return () => {
|
|
4733
4846
|
// Only hand focus back if this modal still owns it: closing a background
|
|
4734
4847
|
// modal must not pull focus out of one that is still open on top. A
|
|
@@ -6158,28 +6271,6 @@ const Loading = ({ active = false, isFullPage = false, size, color, canCancel =
|
|
|
6158
6271
|
return (jsxRuntime.jsxs("div", { className: combinedClasses, role: "alert", "aria-busy": "true", "aria-label": "Loading", ...rest, ref: loadingRef, children: [jsxRuntime.jsx("div", { className: combinedOverlayClasses, onClick: handleOverlayClick, "aria-hidden": "true" }), jsxRuntime.jsxs("div", { className: loadingContentClass, children: [indicator ? (jsxRuntime.jsx("span", { className: loadingIconCustomClass, children: indicator })) : (jsxRuntime.jsx("span", { className: combinedIconClasses })), children && jsxRuntime.jsx("div", { className: loadingTextClass, children: children }), canCancel && (jsxRuntime.jsx("button", { type: "button", className: loadingCancelClass, onClick: onCancel, "aria-label": "Cancel loading", children: "Cancel" }))] })] }));
|
|
6159
6272
|
};
|
|
6160
6273
|
|
|
6161
|
-
// INTERNAL: deliberately not exported from src/index.ts.
|
|
6162
|
-
/** The major version of the React this bundle is running against. */
|
|
6163
|
-
const runningMajor = () => Number(React.version.split('.')[0]);
|
|
6164
|
-
/**
|
|
6165
|
-
* The prop that makes an element inert, spelled for the React in use, or
|
|
6166
|
-
* nothing when `inert` is false.
|
|
6167
|
-
*
|
|
6168
|
-
* Both majors must render `inert=""`, and each warns on the other's spelling
|
|
6169
|
-
* and then drops the attribute. React 19 treats `inert` as a boolean and
|
|
6170
|
-
* rejects the empty string. React 18 does not know the attribute, so it
|
|
6171
|
-
* passes a string through and rejects `true`.
|
|
6172
|
-
*/
|
|
6173
|
-
const inertProps = (inert, major = runningMajor()) => {
|
|
6174
|
-
if (!inert)
|
|
6175
|
-
return {};
|
|
6176
|
-
// The cast is for React 18's spelling: this package's types are React 19's,
|
|
6177
|
-
// where `inert` is a boolean.
|
|
6178
|
-
return (major >= 19
|
|
6179
|
-
? { inert: true }
|
|
6180
|
-
: { inert: '' });
|
|
6181
|
-
};
|
|
6182
|
-
|
|
6183
6274
|
/**
|
|
6184
6275
|
* `null` outside a group. A `Collapse` also resets it to `null` for its own
|
|
6185
6276
|
* subtree, so a Collapse nested in an item's content keeps its own state
|
|
@@ -7277,7 +7368,7 @@ const PopoverComponent = ({ trigger, children, open: controlledOpen, defaultOpen
|
|
|
7277
7368
|
const panel = panelRef.current;
|
|
7278
7369
|
if (!panelMounted || trapFocus || !panel)
|
|
7279
7370
|
return undefined;
|
|
7280
|
-
(
|
|
7371
|
+
focusTabStop(panel);
|
|
7281
7372
|
return () => {
|
|
7282
7373
|
const doc = panel.ownerDocument;
|
|
7283
7374
|
const now = getDeepestActiveElement(doc);
|
|
@@ -8614,7 +8705,7 @@ const SidebarComponent = React.forwardRef(({ isOpen, onClose, position = 'left',
|
|
|
8614
8705
|
// the sidebar's other controls out of reach. It leaves focus alone
|
|
8615
8706
|
// then, and the cleanup below finds focus outside and leaves it too.
|
|
8616
8707
|
if (overlay || !isModalOverlayOpen()) {
|
|
8617
|
-
(
|
|
8708
|
+
focusTabStop(element);
|
|
8618
8709
|
}
|
|
8619
8710
|
return () => {
|
|
8620
8711
|
// Only while the sidebar still holds focus: closing one under another
|
|
@@ -9365,6 +9456,69 @@ const DefaultPrevIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
|
|
|
9365
9456
|
* @returns {JSX.Element} The rendered SVG chevron-right icon.
|
|
9366
9457
|
*/
|
|
9367
9458
|
const DefaultNextIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }));
|
|
9459
|
+
/**
|
|
9460
|
+
* The media queries Bulma's `fullhd`, `widescreen`, `desktop` and `tablet`
|
|
9461
|
+
* mixins open with, widest first, at Bulma's default breakpoints. The
|
|
9462
|
+
* stylesheet picks the count in view with those mixins; these tell the
|
|
9463
|
+
* script which count that is. Narrower than the last one is `mobile`.
|
|
9464
|
+
*/
|
|
9465
|
+
const VIEWPORT_QUERIES = [
|
|
9466
|
+
'screen and (min-width: 1408px)',
|
|
9467
|
+
'screen and (min-width: 1216px)',
|
|
9468
|
+
'screen and (min-width: 1024px)',
|
|
9469
|
+
'screen and (min-width: 769px), print',
|
|
9470
|
+
];
|
|
9471
|
+
/** `useBulmaViewport`'s answer below the tablet breakpoint. */
|
|
9472
|
+
const MOBILE = VIEWPORT_QUERIES.length;
|
|
9473
|
+
/**
|
|
9474
|
+
* The `--bulma-carousel-items-to-show*` variable each count prop writes, in
|
|
9475
|
+
* the order the component lists them: the viewports as above, mobile, and
|
|
9476
|
+
* last `itemsToShow` itself.
|
|
9477
|
+
*/
|
|
9478
|
+
const COUNT_SUFFIXES = [
|
|
9479
|
+
'-fullhd',
|
|
9480
|
+
'-widescreen',
|
|
9481
|
+
'-desktop',
|
|
9482
|
+
'-tablet',
|
|
9483
|
+
'-mobile',
|
|
9484
|
+
'',
|
|
9485
|
+
];
|
|
9486
|
+
/**
|
|
9487
|
+
* Which of Bulma's viewports the window is in: the index of the widest
|
|
9488
|
+
* `VIEWPORT_QUERIES` entry that matches, `MOBILE` below them all, or `-1`
|
|
9489
|
+
* when that isn't known (on the server, before the first layout effect, and
|
|
9490
|
+
* whenever `enabled` is off, so a carousel with one count everywhere
|
|
9491
|
+
* subscribes to nothing).
|
|
9492
|
+
*
|
|
9493
|
+
* @function
|
|
9494
|
+
* @param {boolean} enabled - Whether any viewport-specific count is set.
|
|
9495
|
+
* @returns {number} The viewport, as described above.
|
|
9496
|
+
*/
|
|
9497
|
+
function useBulmaViewport(enabled) {
|
|
9498
|
+
const [viewport, setViewport] = React.useState(-1);
|
|
9499
|
+
useClientLayoutEffect(() => {
|
|
9500
|
+
if (!enabled || typeof window.matchMedia !== 'function')
|
|
9501
|
+
return undefined;
|
|
9502
|
+
const lists = VIEWPORT_QUERIES.map(query => window.matchMedia(query));
|
|
9503
|
+
const update = () => {
|
|
9504
|
+
const index = lists.findIndex(list => list.matches);
|
|
9505
|
+
setViewport(index < 0 ? MOBILE : index);
|
|
9506
|
+
};
|
|
9507
|
+
update();
|
|
9508
|
+
lists.forEach(list => list.addEventListener('change', update));
|
|
9509
|
+
return () => lists.forEach(list => list.removeEventListener('change', update));
|
|
9510
|
+
}, [enabled]);
|
|
9511
|
+
return enabled ? viewport : -1;
|
|
9512
|
+
}
|
|
9513
|
+
/**
|
|
9514
|
+
* A count prop as the whole number the carousel works with: rounded down,
|
|
9515
|
+
* and never below 1, so a zero or a stray `NaN` can't divide by nothing.
|
|
9516
|
+
*
|
|
9517
|
+
* @function
|
|
9518
|
+
* @param {number | undefined} count - The prop as given.
|
|
9519
|
+
* @returns {number | undefined} The count, or `undefined` when unset.
|
|
9520
|
+
*/
|
|
9521
|
+
const wholeCount = (count) => count === undefined ? undefined : Math.max(1, Math.floor(count) || 1);
|
|
9368
9522
|
/**
|
|
9369
9523
|
* The `Carousel` component provides an image/content slider with navigation arrows and indicators.
|
|
9370
9524
|
*
|
|
@@ -9391,15 +9545,28 @@ const DefaultNextIcon = () => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org
|
|
|
9391
9545
|
* <img src="image2.jpg" alt="Slide 2" />
|
|
9392
9546
|
* </CarouselItem>
|
|
9393
9547
|
* </Carousel>
|
|
9548
|
+
*
|
|
9549
|
+
* @example
|
|
9550
|
+
* // Three cards in view on desktop, two on tablet, one on mobile
|
|
9551
|
+
* <Carousel itemsToShow={1} itemsToShowTablet={2} itemsToShowDesktop={3}>
|
|
9552
|
+
* {cards.map(card => (
|
|
9553
|
+
* <CarouselItem key={card.id}>{card.body}</CarouselItem>
|
|
9554
|
+
* ))}
|
|
9555
|
+
* </Carousel>
|
|
9394
9556
|
*/
|
|
9395
|
-
const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, interval = 5000, pauseOnHover = true, repeat = true, hasDrag = true, arrow = true, arrowHover = false, indicator = true, indicatorInside = false, indicatorPosition = 'bottom', indicatorStyle = 'dots', iconPrev, iconNext, iconLibrary, iconVariant, iconSize, iconFeatures, arrowBackground = true, arrowColor, ariaLabel = 'Image carousel', onChange, className, children, ...props }, ref) => {
|
|
9557
|
+
const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, interval = 5000, pauseOnHover = true, repeat = true, hasDrag = true, itemsToShow, itemsToShowMobile, itemsToShowTablet, itemsToShowDesktop, itemsToShowWidescreen, itemsToShowFullhd, itemsToScroll, arrow = true, arrowHover = false, indicator = true, indicatorInside = false, indicatorPosition = 'bottom', indicatorStyle = 'dots', iconPrev, iconNext, iconLibrary, iconVariant, iconSize, iconFeatures, arrowBackground = true, arrowColor, ariaLabel = 'Image carousel', onChange, className, style, children, ...props }, ref) => {
|
|
9396
9558
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
9397
9559
|
const carouselRef = React.useRef(null);
|
|
9398
9560
|
// Cleanup returned by a consumer's callback ref on attach, held until detach.
|
|
9399
9561
|
const consumerCleanupRef = React.useRef(null);
|
|
9400
9562
|
const containerRef = React.useRef(null);
|
|
9563
|
+
const slidesRef = React.useRef(null);
|
|
9564
|
+
// The item (a child of the track) that holds focus, until focus leaves
|
|
9565
|
+
// the items for somewhere else
|
|
9566
|
+
const focusedItemRef = React.useRef(undefined);
|
|
9401
9567
|
const [internalValue, setInternalValue] = React.useState(0);
|
|
9402
9568
|
const [isPaused, setIsPaused] = React.useState(false);
|
|
9569
|
+
const [isFocusInside, setIsFocusInside] = React.useState(false);
|
|
9403
9570
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
9404
9571
|
const [dragStart, setDragStart] = React.useState(0);
|
|
9405
9572
|
const [dragOffset, setDragOffset] = React.useState(0);
|
|
@@ -9409,64 +9576,105 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9409
9576
|
// Get valid children
|
|
9410
9577
|
const items = React.Children.toArray(children).filter(React.isValidElement);
|
|
9411
9578
|
const itemCount = items.length;
|
|
9412
|
-
//
|
|
9413
|
-
|
|
9414
|
-
//
|
|
9415
|
-
//
|
|
9416
|
-
|
|
9579
|
+
// The count in view. The viewport props come widest first, in the order
|
|
9580
|
+
// of VIEWPORT_QUERIES, then mobile; each min-width viewport falls back to
|
|
9581
|
+
// the narrower ones, and every viewport to `itemsToShow`, the cascade the
|
|
9582
|
+
// stylesheet's `--bulma-carousel-items-to-show-*` defaults spell out.
|
|
9583
|
+
const counts = [
|
|
9584
|
+
itemsToShowFullhd,
|
|
9585
|
+
itemsToShowWidescreen,
|
|
9586
|
+
itemsToShowDesktop,
|
|
9587
|
+
itemsToShowTablet,
|
|
9588
|
+
itemsToShowMobile,
|
|
9589
|
+
].map(wholeCount);
|
|
9590
|
+
const baseCount = wholeCount(itemsToShow);
|
|
9591
|
+
const viewport = useBulmaViewport(counts.some(c => c !== undefined));
|
|
9592
|
+
const inView = (viewport === MOBILE
|
|
9593
|
+
? counts[MOBILE]
|
|
9594
|
+
: viewport >= 0
|
|
9595
|
+
? counts.slice(viewport, MOBILE).find(c => c !== undefined)
|
|
9596
|
+
: undefined) ??
|
|
9597
|
+
baseCount ??
|
|
9598
|
+
1;
|
|
9599
|
+
// The props as the stylesheet reads them, so the widths are right from
|
|
9600
|
+
// the first paint, on the server too.
|
|
9601
|
+
const countStyle = {};
|
|
9602
|
+
[...counts, baseCount].forEach((count, i) => {
|
|
9603
|
+
if (count !== undefined) {
|
|
9604
|
+
countStyle[`--bulma-carousel-items-to-show${COUNT_SUFFIXES[i]}`] =
|
|
9605
|
+
count;
|
|
9606
|
+
}
|
|
9607
|
+
});
|
|
9608
|
+
const scroll = Math.min(wholeCount(itemsToScroll) ?? 1, inView);
|
|
9609
|
+
const looping = repeat && itemCount > inView;
|
|
9610
|
+
const lastStart = Math.max(0, itemCount - inView);
|
|
9611
|
+
// Where the view can start: every `scroll` items from the first, and
|
|
9612
|
+
// without a loop the last full view as well, which is what keeps the
|
|
9613
|
+
// end of the track from showing empty space.
|
|
9614
|
+
const positions = [];
|
|
9615
|
+
for (let p = 0; p < (looping ? itemCount : lastStart); p += scroll) {
|
|
9616
|
+
positions.push(p);
|
|
9617
|
+
}
|
|
9618
|
+
if (!looping)
|
|
9619
|
+
positions.push(lastStart);
|
|
9620
|
+
const lastPosition = positions[positions.length - 1];
|
|
9621
|
+
const canMove = positions.length > 1;
|
|
9622
|
+
const wrap = (index) => ((index % itemCount) + itemCount) % itemCount;
|
|
9623
|
+
const fit = (index) => looping ? wrap(index) : Math.min(Math.max(index, 0), lastStart);
|
|
9624
|
+
// Use controlled or internal value, as a start the view can show
|
|
9625
|
+
const activeIndex = fit(controlledValue ?? internalValue);
|
|
9626
|
+
// The indicator for the last position at or before the first item in view
|
|
9627
|
+
const activePosition = positions.reduce((found, position, index) => (position <= activeIndex ? index : found), 0);
|
|
9628
|
+
const nextIndex = positions[activePosition + 1];
|
|
9629
|
+
// From between two positions, the previous step is the one just behind
|
|
9630
|
+
const prevIndex = activeIndex > positions[activePosition]
|
|
9631
|
+
? positions[activePosition]
|
|
9632
|
+
: positions[activePosition - 1];
|
|
9633
|
+
// Sync displayIndex when the value changes externally, or when the count
|
|
9634
|
+
// in view moves the start it fits to. Intentional external-prop sync that
|
|
9635
|
+
// must not run mid-wrap animation; covered by tests and certified
|
|
9636
|
+
// in-browser. Uncontrolled, the fitted start becomes the value too, so a
|
|
9637
|
+
// later change of count keeps what was first in view in view.
|
|
9417
9638
|
React.useEffect(() => {
|
|
9418
|
-
if (
|
|
9639
|
+
if (!isWrapping) {
|
|
9419
9640
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncing to controlled prop
|
|
9420
|
-
setDisplayIndex(
|
|
9641
|
+
setDisplayIndex(activeIndex);
|
|
9642
|
+
if (controlledValue === undefined)
|
|
9643
|
+
setInternalValue(activeIndex);
|
|
9421
9644
|
}
|
|
9422
|
-
}, [
|
|
9645
|
+
}, [activeIndex, isWrapping, controlledValue]);
|
|
9423
9646
|
// Update active index
|
|
9424
|
-
const setActiveIndex = React.useCallback((index
|
|
9425
|
-
let newIndex = index;
|
|
9426
|
-
if (repeat) {
|
|
9427
|
-
if (index < 0) {
|
|
9428
|
-
newIndex = itemCount - 1;
|
|
9429
|
-
}
|
|
9430
|
-
else if (index >= itemCount) {
|
|
9431
|
-
newIndex = 0;
|
|
9432
|
-
}
|
|
9433
|
-
}
|
|
9434
|
-
else {
|
|
9435
|
-
newIndex = Math.max(0, Math.min(index, itemCount - 1));
|
|
9436
|
-
}
|
|
9647
|
+
const setActiveIndex = React.useCallback((index) => {
|
|
9437
9648
|
if (controlledValue === undefined) {
|
|
9438
|
-
setInternalValue(
|
|
9439
|
-
}
|
|
9440
|
-
if (syncDisplay) {
|
|
9441
|
-
setDisplayIndex(newIndex);
|
|
9649
|
+
setInternalValue(index);
|
|
9442
9650
|
}
|
|
9443
|
-
|
|
9444
|
-
|
|
9651
|
+
setDisplayIndex(index);
|
|
9652
|
+
setIsWrapping(false);
|
|
9653
|
+
onChange?.(index);
|
|
9654
|
+
}, [controlledValue, onChange]);
|
|
9445
9655
|
// Navigation handlers
|
|
9446
9656
|
const goToPrev = React.useCallback(() => {
|
|
9447
|
-
if (
|
|
9448
|
-
|
|
9449
|
-
setDisplayIndex(-1);
|
|
9450
|
-
setIsWrapping(true);
|
|
9657
|
+
if (prevIndex !== undefined) {
|
|
9658
|
+
setActiveIndex(prevIndex);
|
|
9451
9659
|
}
|
|
9452
|
-
else {
|
|
9453
|
-
|
|
9660
|
+
else if (looping) {
|
|
9661
|
+
// Animate to the clones before the first item, which show the last
|
|
9662
|
+
// position, and snap to the real one when the transition ends
|
|
9663
|
+
setDisplayIndex(lastPosition - itemCount);
|
|
9664
|
+
setIsWrapping(true);
|
|
9454
9665
|
}
|
|
9455
|
-
}, [
|
|
9666
|
+
}, [prevIndex, looping, lastPosition, itemCount, setActiveIndex]);
|
|
9456
9667
|
const goToNext = React.useCallback(() => {
|
|
9457
|
-
if (
|
|
9458
|
-
|
|
9668
|
+
if (nextIndex !== undefined) {
|
|
9669
|
+
setActiveIndex(nextIndex);
|
|
9670
|
+
}
|
|
9671
|
+
else if (looping) {
|
|
9672
|
+
// Animate to the clones after the last item, which show the first
|
|
9673
|
+
// position, and snap to the real one when the transition ends
|
|
9459
9674
|
setDisplayIndex(itemCount);
|
|
9460
9675
|
setIsWrapping(true);
|
|
9461
9676
|
}
|
|
9462
|
-
|
|
9463
|
-
setActiveIndex(activeIndex + 1);
|
|
9464
|
-
}
|
|
9465
|
-
}, [activeIndex, itemCount, repeat, setActiveIndex]);
|
|
9466
|
-
const goToSlide = React.useCallback((index) => {
|
|
9467
|
-
setIsWrapping(false);
|
|
9468
|
-
setActiveIndex(index);
|
|
9469
|
-
}, [setActiveIndex]);
|
|
9677
|
+
}, [nextIndex, looping, itemCount, setActiveIndex]);
|
|
9470
9678
|
// Read when the timer fires or an arrow key arrives, so neither the
|
|
9471
9679
|
// timer nor the key listener starts over while `onChange` changes
|
|
9472
9680
|
// identity, as an inline one does on every render of the parent.
|
|
@@ -9474,15 +9682,28 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9474
9682
|
const goToNextRef = useLatestRef(goToNext);
|
|
9475
9683
|
// Auto-play. Each slide gets the whole interval, so a slide change,
|
|
9476
9684
|
// autoplay's own or the user's, starts the timer over. A render that
|
|
9477
|
-
// leaves the slide where it is doesn't.
|
|
9685
|
+
// leaves the slide where it is doesn't. It waits while focus is inside
|
|
9686
|
+
// an item, so what someone is reading or using doesn't move away. The
|
|
9687
|
+
// timer reads where focus is when it fires, rather than trusting the
|
|
9688
|
+
// last focus event alone: a browser moving focus off an element that
|
|
9689
|
+
// went inert need not fire one, and the pause would never end. A focus
|
|
9690
|
+
// change still starts the timer over, so an item gets the whole
|
|
9691
|
+
// interval once focus leaves it.
|
|
9692
|
+
const isPlaying = autoplay && (looping || nextIndex !== undefined) && !isPaused;
|
|
9693
|
+
const isRotating = isPlaying && !isFocusInside;
|
|
9478
9694
|
React.useEffect(() => {
|
|
9479
|
-
if (!
|
|
9695
|
+
if (!isPlaying)
|
|
9480
9696
|
return undefined;
|
|
9481
9697
|
const timer = setInterval(() => {
|
|
9698
|
+
const track = slidesRef.current;
|
|
9699
|
+
if (track?.contains(getActiveElementInTree(track)))
|
|
9700
|
+
return;
|
|
9701
|
+
if (isFocusInside)
|
|
9702
|
+
setIsFocusInside(false);
|
|
9482
9703
|
goToNextRef.current();
|
|
9483
9704
|
}, interval);
|
|
9484
9705
|
return () => clearInterval(timer);
|
|
9485
|
-
}, [
|
|
9706
|
+
}, [isPlaying, isFocusInside, interval, activeIndex, goToNextRef]);
|
|
9486
9707
|
// Pause on hover
|
|
9487
9708
|
const handleMouseEnter = React.useCallback(() => {
|
|
9488
9709
|
if (pauseOnHover) {
|
|
@@ -9541,12 +9762,16 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9541
9762
|
const handleTouchEnd = React.useCallback(() => {
|
|
9542
9763
|
handleDragEnd();
|
|
9543
9764
|
}, [handleDragEnd]);
|
|
9544
|
-
// Keyboard navigation
|
|
9765
|
+
// Keyboard navigation, from the carousel's own controls. Inside an item
|
|
9766
|
+
// the keys are its content's (a field's caret, a slider), and a step
|
|
9767
|
+
// from there could take the focused item out of view.
|
|
9545
9768
|
React.useEffect(() => {
|
|
9546
9769
|
const handleKeyDown = (e) => {
|
|
9547
9770
|
const node = carouselRef.current;
|
|
9548
|
-
|
|
9771
|
+
const active = getActiveElementInTree(node);
|
|
9772
|
+
if (!node?.contains(active) || slidesRef.current?.contains(active)) {
|
|
9549
9773
|
return;
|
|
9774
|
+
}
|
|
9550
9775
|
switch (e.key) {
|
|
9551
9776
|
case 'ArrowLeft':
|
|
9552
9777
|
e.preventDefault();
|
|
@@ -9561,22 +9786,14 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9561
9786
|
document.addEventListener('keydown', handleKeyDown);
|
|
9562
9787
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
9563
9788
|
}, [goToPrevRef, goToNextRef]);
|
|
9564
|
-
// Handle wrap-around transition end
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9789
|
+
// Handle wrap-around transition end: the track's own, since an item's
|
|
9790
|
+
// content can end a transition of its own that bubbles up to here
|
|
9791
|
+
const handleTransitionEnd = React.useCallback((e) => {
|
|
9792
|
+
if (isWrapping && e.target === e.currentTarget) {
|
|
9793
|
+
// Skip transition for the instant snap from the clones to the
|
|
9794
|
+
// real items they copy
|
|
9568
9795
|
setSkipTransition(true);
|
|
9569
|
-
|
|
9570
|
-
if (displayIndex === itemCount) {
|
|
9571
|
-
// Was at clone of first slide, snap to real first slide
|
|
9572
|
-
setActiveIndex(0, false);
|
|
9573
|
-
setDisplayIndex(0);
|
|
9574
|
-
}
|
|
9575
|
-
else if (displayIndex === -1) {
|
|
9576
|
-
// Was at clone of last slide, snap to real last slide
|
|
9577
|
-
setActiveIndex(itemCount - 1, false);
|
|
9578
|
-
setDisplayIndex(itemCount - 1);
|
|
9579
|
-
}
|
|
9796
|
+
setActiveIndex(((displayIndex % itemCount) + itemCount) % itemCount);
|
|
9580
9797
|
// Re-enable transition after the snap
|
|
9581
9798
|
requestAnimationFrame(() => {
|
|
9582
9799
|
requestAnimationFrame(() => {
|
|
@@ -9618,8 +9835,8 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9618
9835
|
}
|
|
9619
9836
|
}, [ref]);
|
|
9620
9837
|
// Can navigate
|
|
9621
|
-
const canGoPrev =
|
|
9622
|
-
const canGoNext =
|
|
9838
|
+
const canGoPrev = looping || prevIndex !== undefined;
|
|
9839
|
+
const canGoNext = looping || nextIndex !== undefined;
|
|
9623
9840
|
// Generate classes
|
|
9624
9841
|
const carouselClasses = usePrefixedClassNames('carousel', {
|
|
9625
9842
|
'is-overlay': indicatorInside,
|
|
@@ -9637,50 +9854,101 @@ const Carousel = React.forwardRef(({ value: controlledValue, autoplay = false, i
|
|
|
9637
9854
|
'is-top': indicatorPosition === 'top',
|
|
9638
9855
|
});
|
|
9639
9856
|
const combinedClasses = classNames(carouselClasses, bulmaHelperClasses, className);
|
|
9640
|
-
//
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9857
|
+
// Where the track is drawn from: mid-wrap the clones, otherwise a start
|
|
9858
|
+
// the view can show, so a change in the count in view never paints
|
|
9859
|
+
// empty space while the sync above catches up.
|
|
9860
|
+
const shownIndex = isWrapping ? displayIndex : fit(displayIndex);
|
|
9861
|
+
// Enough clones on each side to fill the widest view any viewport sets,
|
|
9862
|
+
// since the stylesheet, not this count, sizes what's on screen
|
|
9863
|
+
const cloneCount = looping
|
|
9864
|
+
? Math.min(itemCount, Math.max(baseCount ?? 1, ...counts.map(c => c ?? 1)))
|
|
9865
|
+
: 0;
|
|
9866
|
+
// The track: a full view's worth of clones of the last items, the items,
|
|
9867
|
+
// and clones of the first ones, for a seamless loop. Each entry is the
|
|
9868
|
+
// item it shows and its key.
|
|
9869
|
+
const slots = items.map((_, index) => [
|
|
9870
|
+
index,
|
|
9871
|
+
index,
|
|
9872
|
+
]);
|
|
9873
|
+
for (let k = 0; k < cloneCount; k++) {
|
|
9874
|
+
slots.splice(k, 0, [itemCount - cloneCount + k, `clone-before-${k}`]);
|
|
9875
|
+
slots.push([k, `clone-after-${k}`]);
|
|
9876
|
+
}
|
|
9877
|
+
// Render slides with active state (includes clones for infinite loop).
|
|
9878
|
+
// What's in view is a run of slots, so where a looping view runs past
|
|
9879
|
+
// the last item, the clones on screen are the live copies and the items
|
|
9880
|
+
// they copy, off screen, are the hidden ones: no item is exposed twice,
|
|
9881
|
+
// and the copy exposed is the one in view.
|
|
9882
|
+
const renderSlides = () => slots.map(([index, key], slot) => {
|
|
9883
|
+
const item = items[index];
|
|
9884
|
+
const own = item.props;
|
|
9885
|
+
const isShown = slot >= shownIndex + cloneCount &&
|
|
9886
|
+
slot < shownIndex + cloneCount + inView;
|
|
9887
|
+
return React.cloneElement(item, {
|
|
9888
|
+
key,
|
|
9889
|
+
active: isShown,
|
|
9890
|
+
role: own.role ?? 'group',
|
|
9891
|
+
'aria-roledescription': own['aria-roledescription'] ?? 'slide',
|
|
9892
|
+
...(own['aria-label'] || own['aria-labelledby']
|
|
9893
|
+
? {}
|
|
9894
|
+
: { 'aria-label': `${index + 1} of ${itemCount}` }),
|
|
9895
|
+
// Out of view: hidden from assistive tech, and inert, so nothing
|
|
9896
|
+
// in it takes focus or a click
|
|
9897
|
+
...(isShown ? {} : { 'aria-hidden': true, ...inertProps(true) }),
|
|
9650
9898
|
});
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9899
|
+
});
|
|
9900
|
+
// Hand focus on when the item holding it leaves the view. An item out
|
|
9901
|
+
// of view is inert, and a browser drops focus from an inert element to
|
|
9902
|
+
// the page, so a swipe, a change of count with the window's width, a new
|
|
9903
|
+
// controlled value or the item's removal would leave a keyboard or
|
|
9904
|
+
// screen reader user nowhere. Focus goes to the first item in view,
|
|
9905
|
+
// given `tabindex="-1"` for it unless it has a tabindex of its own. A
|
|
9906
|
+
// layout effect runs before the browser gets to drop the focus; the
|
|
9907
|
+
// focused item's removal drops it at once, so focus on the page itself
|
|
9908
|
+
// still counts as the item's.
|
|
9909
|
+
useClientLayoutEffect(() => {
|
|
9910
|
+
const held = focusedItemRef.current;
|
|
9911
|
+
if (!held)
|
|
9912
|
+
return;
|
|
9913
|
+
const track = slidesRef.current;
|
|
9914
|
+
const start = shownIndex + cloneCount;
|
|
9915
|
+
const slot = Array.prototype.indexOf.call(track.children, held);
|
|
9916
|
+
if (slot >= start && slot < start + inView)
|
|
9917
|
+
return;
|
|
9918
|
+
const active = getActiveElementInTree(track);
|
|
9919
|
+
if (active !== held.ownerDocument.body && !held.contains(active)) {
|
|
9920
|
+
return;
|
|
9668
9921
|
}
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9922
|
+
focusedItemRef.current = undefined;
|
|
9923
|
+
const first = track.children[start];
|
|
9924
|
+
if (!first)
|
|
9925
|
+
return;
|
|
9926
|
+
if (!first.hasAttribute('tabindex'))
|
|
9927
|
+
first.tabIndex = -1;
|
|
9928
|
+
first.focus({ preventScroll: true });
|
|
9929
|
+
}, [shownIndex, cloneCount, inView, itemCount]);
|
|
9930
|
+
return (jsxRuntime.jsxs("div", { ref: combinedRef, className: combinedClasses, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, role: "region", "aria-roledescription": "carousel", "aria-label": ariaLabel, style: { ...countStyle, ...style }, ...rest, children: [jsxRuntime.jsxs("div", { ref: containerRef, className: carouselContainerClass, onMouseDown: hasDrag ? handleMouseDown : undefined, onMouseMove: hasDrag && isDragging ? handleMouseMove : undefined, onMouseUp: hasDrag ? handleMouseUp : undefined, onMouseLeave: hasDrag ? handleMouseUp : undefined, onTouchStart: hasDrag ? handleTouchStart : undefined, onTouchMove: hasDrag ? handleTouchMove : undefined, onTouchEnd: hasDrag ? handleTouchEnd : undefined, style: {
|
|
9672
9931
|
transform: isDragging ? `translateX(${dragOffset}px)` : undefined,
|
|
9673
9932
|
cursor: hasDrag ? (isDragging ? 'grabbing' : 'grab') : undefined,
|
|
9674
|
-
}, children: [jsxRuntime.jsx("div", { className: carouselSlidesClass, style: {
|
|
9675
|
-
//
|
|
9676
|
-
|
|
9933
|
+
}, children: [jsxRuntime.jsx("div", { ref: slidesRef, className: carouselSlidesClass, style: {
|
|
9934
|
+
// One item is the track's width plus a gap, over the count the
|
|
9935
|
+
// stylesheet has in view; account for the prepended clones
|
|
9936
|
+
transform: `translateX(calc(${-(shownIndex + cloneCount)} * (100% + var(--bulma-carousel-item-gap, 0px)) / var(--bulma-carousel-items-in-view, 1)))`,
|
|
9677
9937
|
// Disable transition during drag or instant snap after wrap-around
|
|
9678
9938
|
transition: isDragging || skipTransition
|
|
9679
9939
|
? 'none'
|
|
9680
9940
|
: 'transform 0.3s ease-in-out',
|
|
9681
|
-
},
|
|
9682
|
-
|
|
9683
|
-
|
|
9941
|
+
}, "aria-live": isRotating ? 'off' : 'polite', onFocus: e => {
|
|
9942
|
+
setIsFocusInside(true);
|
|
9943
|
+
focusedItemRef.current = Array.from(e.currentTarget.children).find(item => item.contains(e.target));
|
|
9944
|
+
}, onBlur: e => {
|
|
9945
|
+
const inside = e.currentTarget.contains(e.relatedTarget);
|
|
9946
|
+
setIsFocusInside(inside);
|
|
9947
|
+
if (!inside)
|
|
9948
|
+
focusedItemRef.current = undefined;
|
|
9949
|
+
}, onTransitionEnd: handleTransitionEnd, children: renderSlides() }), arrow && canMove && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Button, { type: "button", className: carouselArrowPrevClass, onClick: goToPrev, isDisabled: !canGoPrev, "aria-label": "Previous slide", children: iconPrev ? (jsxRuntime.jsx(Icon, { name: iconPrev, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })) : (jsxRuntime.jsx(DefaultPrevIcon, {})) }), jsxRuntime.jsx(Button, { type: "button", className: carouselArrowNextClass, onClick: goToNext, isDisabled: !canGoNext, "aria-label": "Next slide", children: iconNext ? (jsxRuntime.jsx(Icon, { name: iconNext, library: iconLibrary, variant: iconVariant, size: iconSize, features: iconFeatures })) : (jsxRuntime.jsx(DefaultNextIcon, {})) })] }))] }), indicator && canMove && (jsxRuntime.jsx("div", { className: carouselIndicatorClass, role: "tablist", children: positions.map((position, index) => (jsxRuntime.jsx(Button, { type: "button", className: prefixedClassNames(classPrefix, 'indicator-item', {
|
|
9950
|
+
'is-active': index === activePosition,
|
|
9951
|
+
}), onClick: () => setActiveIndex(position), role: "tab", "aria-selected": index === activePosition, "aria-label": `Go to slide ${position + 1}` }, position))) }))] }));
|
|
9684
9952
|
});
|
|
9685
9953
|
Carousel.displayName = 'Carousel';
|
|
9686
9954
|
|
|
@@ -11163,23 +11431,15 @@ const Image = ({ as, className, textColor, color, bgColor, size, isRounded, isRe
|
|
|
11163
11431
|
backgroundColor: bgColor,
|
|
11164
11432
|
...props,
|
|
11165
11433
|
});
|
|
11434
|
+
// Bulma reads `.has-ratio` only on a child of the ratio container, so the
|
|
11435
|
+
// container carries just its `is-<ratio>` class.
|
|
11166
11436
|
const bulmaClasses = usePrefixedClassNames('image', {
|
|
11167
11437
|
[`is-${size}`]: size,
|
|
11168
|
-
'has-ratio': size && typeof size === 'string' && size.includes('by'),
|
|
11169
11438
|
});
|
|
11170
11439
|
const imageClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
11171
11440
|
// Default tag logic: if "as" is provided, use it.
|
|
11172
|
-
// If not, use <figure> for aspect ratios
|
|
11173
|
-
|
|
11174
|
-
if (as) {
|
|
11175
|
-
Tag = as;
|
|
11176
|
-
}
|
|
11177
|
-
else if (size && typeof size === 'string' && size.includes('by')) {
|
|
11178
|
-
Tag = 'figure';
|
|
11179
|
-
}
|
|
11180
|
-
else {
|
|
11181
|
-
Tag = 'div';
|
|
11182
|
-
}
|
|
11441
|
+
// If not, use <figure> for aspect ratios, <div> otherwise.
|
|
11442
|
+
const Tag = as ?? (size?.includes('by') ? 'figure' : 'div');
|
|
11183
11443
|
const roundedClass = usePrefixedClassNames('is-rounded');
|
|
11184
11444
|
const content = children ? (children) : (jsxRuntime.jsx("img", { className: classNames({ [roundedClass]: isRounded }) || undefined, src: src, alt: alt, ...(isRetina && src ? { srcSet: `${src} 2x` } : {}) }));
|
|
11185
11445
|
return (jsxRuntime.jsx(Tag, { className: imageClasses, ...rest, children: content }));
|
|
@@ -21896,6 +22156,7 @@ exports.Menu = Menu;
|
|
|
21896
22156
|
exports.MenuItem = MenuItem;
|
|
21897
22157
|
exports.MenuLabel = MenuLabel;
|
|
21898
22158
|
exports.MenuList = MenuList;
|
|
22159
|
+
exports.MenuSection = MenuSection;
|
|
21899
22160
|
exports.Message = Message;
|
|
21900
22161
|
exports.Modal = Modal;
|
|
21901
22162
|
exports.Navbar = Navbar;
|