@allxsmith/bestax-bulma 5.55.0 → 5.57.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/columns/Column.js +1 -0
- package/dist/esm/columns/Column.js.map +1 -1
- package/dist/esm/columns/Columns.js +1 -0
- package/dist/esm/columns/Columns.js.map +1 -1
- package/dist/esm/components/Avatar.js +1 -0
- package/dist/esm/components/Avatar.js.map +1 -1
- package/dist/esm/components/Avatars.js +1 -0
- package/dist/esm/components/Avatars.js.map +1 -1
- package/dist/esm/components/Badge.js +1 -0
- package/dist/esm/components/Badge.js.map +1 -1
- package/dist/esm/components/Breadcrumb.js +1 -0
- package/dist/esm/components/Breadcrumb.js.map +1 -1
- package/dist/esm/components/Card.js +1 -0
- package/dist/esm/components/Card.js.map +1 -1
- package/dist/esm/components/Carousel.js +1 -0
- package/dist/esm/components/Carousel.js.map +1 -1
- package/dist/esm/components/Collapse.js +1 -0
- package/dist/esm/components/Collapse.js.map +1 -1
- package/dist/esm/components/Collapses.js +1 -0
- package/dist/esm/components/Collapses.js.map +1 -1
- package/dist/esm/components/Command.js +1 -0
- package/dist/esm/components/Command.js.map +1 -1
- package/dist/esm/components/ContextMenu.js +1 -0
- package/dist/esm/components/ContextMenu.js.map +1 -1
- package/dist/esm/components/DataTable.js +1 -0
- package/dist/esm/components/DataTable.js.map +1 -1
- package/dist/esm/components/Dialog.js +1 -0
- package/dist/esm/components/Dialog.js.map +1 -1
- package/dist/esm/components/Dropdown.js +1 -0
- package/dist/esm/components/Dropdown.js.map +1 -1
- package/dist/esm/components/EmptyState.js +1 -0
- package/dist/esm/components/EmptyState.js.map +1 -1
- package/dist/esm/components/HoverCard.js +1 -0
- package/dist/esm/components/HoverCard.js.map +1 -1
- package/dist/esm/components/Loading.js +1 -0
- package/dist/esm/components/Loading.js.map +1 -1
- package/dist/esm/components/Menu.js +1 -0
- package/dist/esm/components/Menu.js.map +1 -1
- package/dist/esm/components/Message.js +1 -0
- package/dist/esm/components/Message.js.map +1 -1
- package/dist/esm/components/Modal.js +1 -0
- package/dist/esm/components/Modal.js.map +1 -1
- package/dist/esm/components/Navbar.js +1 -0
- package/dist/esm/components/Navbar.js.map +1 -1
- package/dist/esm/components/Pagination.js +1 -0
- package/dist/esm/components/Pagination.js.map +1 -1
- package/dist/esm/components/Panel.js +1 -0
- package/dist/esm/components/Panel.js.map +1 -1
- package/dist/esm/components/Popover.js +1 -0
- package/dist/esm/components/Popover.js.map +1 -1
- package/dist/esm/components/Reveal.js +1 -0
- package/dist/esm/components/Reveal.js.map +1 -1
- package/dist/esm/components/ScrollArea.js +1 -0
- package/dist/esm/components/ScrollArea.js.map +1 -1
- package/dist/esm/components/SegmentedControl.js +1 -0
- package/dist/esm/components/SegmentedControl.js.map +1 -1
- package/dist/esm/components/Sidebar.js +1 -0
- package/dist/esm/components/Sidebar.js.map +1 -1
- package/dist/esm/components/Steps.js +1 -0
- package/dist/esm/components/Steps.js.map +1 -1
- package/dist/esm/components/Tabs.js +1 -0
- package/dist/esm/components/Tabs.js.map +1 -1
- package/dist/esm/components/Timeline.js +1 -0
- package/dist/esm/components/Timeline.js.map +1 -1
- package/dist/esm/components/Toast.js +1 -0
- package/dist/esm/components/Toast.js.map +1 -1
- package/dist/esm/components/Tooltip.js +1 -0
- package/dist/esm/components/Tooltip.js.map +1 -1
- package/dist/esm/components/TreeView.js +1 -0
- package/dist/esm/components/TreeView.js.map +1 -1
- package/dist/esm/components/collapsesContext.js +1 -0
- package/dist/esm/components/collapsesContext.js.map +1 -1
- package/dist/esm/components/useDataTable.js +28 -0
- package/dist/esm/components/useDataTable.js.map +1 -1
- package/dist/esm/elements/Block.js +1 -0
- package/dist/esm/elements/Block.js.map +1 -1
- package/dist/esm/elements/Box.js +1 -0
- package/dist/esm/elements/Box.js.map +1 -1
- package/dist/esm/elements/Button.js +1 -0
- package/dist/esm/elements/Button.js.map +1 -1
- package/dist/esm/elements/Buttons.js +1 -0
- package/dist/esm/elements/Buttons.js.map +1 -1
- package/dist/esm/elements/Code.js +1 -0
- package/dist/esm/elements/Code.js.map +1 -1
- package/dist/esm/elements/ColorSwatch.js +1 -0
- package/dist/esm/elements/ColorSwatch.js.map +1 -1
- package/dist/esm/elements/Content.js +1 -0
- package/dist/esm/elements/Content.js.map +1 -1
- package/dist/esm/elements/Delete.js +1 -0
- package/dist/esm/elements/Delete.js.map +1 -1
- package/dist/esm/elements/DescriptionList.js +1 -0
- package/dist/esm/elements/DescriptionList.js.map +1 -1
- package/dist/esm/elements/Divider.js +1 -0
- package/dist/esm/elements/Divider.js.map +1 -1
- package/dist/esm/elements/Emphasis.js +1 -0
- package/dist/esm/elements/Emphasis.js.map +1 -1
- package/dist/esm/elements/Figure.js +1 -0
- package/dist/esm/elements/Figure.js.map +1 -1
- package/dist/esm/elements/Icon.js +1 -0
- package/dist/esm/elements/Icon.js.map +1 -1
- package/dist/esm/elements/IconText.js +1 -0
- package/dist/esm/elements/IconText.js.map +1 -1
- package/dist/esm/elements/Image.js +1 -0
- package/dist/esm/elements/Image.js.map +1 -1
- package/dist/esm/elements/Kbd.js +1 -0
- package/dist/esm/elements/Kbd.js.map +1 -1
- package/dist/esm/elements/Link.js +1 -0
- package/dist/esm/elements/Link.js.map +1 -1
- package/dist/esm/elements/LinkButton.js +1 -0
- package/dist/esm/elements/LinkButton.js.map +1 -1
- package/dist/esm/elements/ListItem.js +1 -0
- package/dist/esm/elements/ListItem.js.map +1 -1
- package/dist/esm/elements/Loader.js +1 -0
- package/dist/esm/elements/Loader.js.map +1 -1
- package/dist/esm/elements/Notification.js +1 -0
- package/dist/esm/elements/Notification.js.map +1 -1
- package/dist/esm/elements/OrderedList.js +1 -0
- package/dist/esm/elements/OrderedList.js.map +1 -1
- package/dist/esm/elements/Paragraph.js +1 -0
- package/dist/esm/elements/Paragraph.js.map +1 -1
- package/dist/esm/elements/Pre.js +1 -0
- package/dist/esm/elements/Pre.js.map +1 -1
- package/dist/esm/elements/Progress.js +1 -0
- package/dist/esm/elements/Progress.js.map +1 -1
- package/dist/esm/elements/RingProgress.js +1 -0
- package/dist/esm/elements/RingProgress.js.map +1 -1
- package/dist/esm/elements/Skeleton.js +1 -0
- package/dist/esm/elements/Skeleton.js.map +1 -1
- package/dist/esm/elements/Span.js +1 -0
- package/dist/esm/elements/Span.js.map +1 -1
- package/dist/esm/elements/Strong.js +1 -0
- package/dist/esm/elements/Strong.js.map +1 -1
- package/dist/esm/elements/SubTitle.js +1 -0
- package/dist/esm/elements/SubTitle.js.map +1 -1
- package/dist/esm/elements/Table.js +1 -0
- package/dist/esm/elements/Table.js.map +1 -1
- package/dist/esm/elements/Tag.js +1 -0
- package/dist/esm/elements/Tag.js.map +1 -1
- package/dist/esm/elements/Tags.js +1 -0
- package/dist/esm/elements/Tags.js.map +1 -1
- package/dist/esm/elements/Tbody.js +1 -0
- package/dist/esm/elements/Tbody.js.map +1 -1
- package/dist/esm/elements/Td.js +1 -0
- package/dist/esm/elements/Td.js.map +1 -1
- package/dist/esm/elements/Tfoot.js +1 -0
- package/dist/esm/elements/Tfoot.js.map +1 -1
- package/dist/esm/elements/Th.js +1 -0
- package/dist/esm/elements/Th.js.map +1 -1
- package/dist/esm/elements/Thead.js +1 -0
- package/dist/esm/elements/Thead.js.map +1 -1
- package/dist/esm/elements/Title.js +1 -0
- package/dist/esm/elements/Title.js.map +1 -1
- package/dist/esm/elements/ToggleButton.js +1 -0
- package/dist/esm/elements/ToggleButton.js.map +1 -1
- package/dist/esm/elements/Tr.js +1 -0
- package/dist/esm/elements/Tr.js.map +1 -1
- package/dist/esm/elements/UnorderedList.js +1 -0
- package/dist/esm/elements/UnorderedList.js.map +1 -1
- package/dist/esm/elements/iconProps.js +1 -0
- package/dist/esm/elements/iconProps.js.map +1 -1
- package/dist/esm/form/Autocomplete.js +1 -0
- package/dist/esm/form/Autocomplete.js.map +1 -1
- package/dist/esm/form/Checkbox.js +1 -0
- package/dist/esm/form/Checkbox.js.map +1 -1
- package/dist/esm/form/CheckboxCard.js +1 -0
- package/dist/esm/form/CheckboxCard.js.map +1 -1
- package/dist/esm/form/Checkboxes.js +1 -0
- package/dist/esm/form/Checkboxes.js.map +1 -1
- package/dist/esm/form/ColorInput.js +69 -0
- package/dist/esm/form/ColorInput.js.map +1 -0
- package/dist/esm/form/ColorInputBase.js +199 -0
- package/dist/esm/form/ColorInputBase.js.map +1 -0
- package/dist/esm/form/ColorPicker.js +5 -20
- package/dist/esm/form/ColorPicker.js.map +1 -1
- package/dist/esm/form/Combobox.js +1 -0
- package/dist/esm/form/Combobox.js.map +1 -1
- package/dist/esm/form/Control.js +1 -0
- package/dist/esm/form/Control.js.map +1 -1
- package/dist/esm/form/DateInput.js +1 -0
- package/dist/esm/form/DateInput.js.map +1 -1
- package/dist/esm/form/DateInputBase.js +1 -0
- package/dist/esm/form/DateInputBase.js.map +1 -1
- package/dist/esm/form/DateRangeInput.js +1 -0
- package/dist/esm/form/DateRangeInput.js.map +1 -1
- package/dist/esm/form/DateRangeInputBase.js +1 -0
- package/dist/esm/form/DateRangeInputBase.js.map +1 -1
- package/dist/esm/form/DateTimeInput.js +1 -0
- package/dist/esm/form/DateTimeInput.js.map +1 -1
- package/dist/esm/form/DateTimeInputBase.js +1 -0
- package/dist/esm/form/DateTimeInputBase.js.map +1 -1
- package/dist/esm/form/Dropzone.js +1 -0
- package/dist/esm/form/Dropzone.js.map +1 -1
- package/dist/esm/form/Field.js +1 -0
- package/dist/esm/form/Field.js.map +1 -1
- package/dist/esm/form/Fieldset.js +1 -0
- package/dist/esm/form/Fieldset.js.map +1 -1
- package/dist/esm/form/File.js +1 -0
- package/dist/esm/form/File.js.map +1 -1
- package/dist/esm/form/FormContext.js +1 -0
- package/dist/esm/form/FormContext.js.map +1 -1
- package/dist/esm/form/Input.js +1 -0
- package/dist/esm/form/Input.js.map +1 -1
- package/dist/esm/form/InputBase.js +1 -0
- package/dist/esm/form/InputBase.js.map +1 -1
- package/dist/esm/form/Numberinput.js +1 -0
- package/dist/esm/form/Numberinput.js.map +1 -1
- package/dist/esm/form/PinInput.js +1 -0
- package/dist/esm/form/PinInput.js.map +1 -1
- package/dist/esm/form/Radio.js +1 -0
- package/dist/esm/form/Radio.js.map +1 -1
- package/dist/esm/form/RadioCard.js +1 -0
- package/dist/esm/form/RadioCard.js.map +1 -1
- package/dist/esm/form/Radios.js +1 -0
- package/dist/esm/form/Radios.js.map +1 -1
- package/dist/esm/form/Rate.js +1 -0
- package/dist/esm/form/Rate.js.map +1 -1
- package/dist/esm/form/Select.js +1 -0
- package/dist/esm/form/Select.js.map +1 -1
- package/dist/esm/form/SelectBase.js +1 -0
- package/dist/esm/form/SelectBase.js.map +1 -1
- package/dist/esm/form/Slider.js +1 -0
- package/dist/esm/form/Slider.js.map +1 -1
- package/dist/esm/form/Switch.js +1 -0
- package/dist/esm/form/Switch.js.map +1 -1
- package/dist/esm/form/Taginput.js +1 -0
- package/dist/esm/form/Taginput.js.map +1 -1
- package/dist/esm/form/TextArea.js +1 -0
- package/dist/esm/form/TextArea.js.map +1 -1
- package/dist/esm/form/TextAreaBase.js +1 -0
- package/dist/esm/form/TextAreaBase.js.map +1 -1
- package/dist/esm/form/TimeInput.js +1 -0
- package/dist/esm/form/TimeInput.js.map +1 -1
- package/dist/esm/form/TimeInputBase.js +1 -0
- package/dist/esm/form/TimeInputBase.js.map +1 -1
- package/dist/esm/form/_colorInternals/EyeDropperButton.js +41 -0
- package/dist/esm/form/_colorInternals/EyeDropperButton.js.map +1 -0
- package/dist/esm/form/_colorInternals/pickerState.js +5 -4
- package/dist/esm/form/_colorInternals/pickerState.js.map +1 -1
- package/dist/esm/form/_pickerInternals/Calendar.js +1 -0
- package/dist/esm/form/_pickerInternals/Calendar.js.map +1 -1
- package/dist/esm/form/_pickerInternals/PickerPopover.js +1 -0
- package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -1
- package/dist/esm/form/_pickerInternals/TimeWheels.js +1 -0
- package/dist/esm/form/_pickerInternals/TimeWheels.js.map +1 -1
- package/dist/esm/form/_pickerInternals/useLocaleDefaults.js +1 -0
- package/dist/esm/form/_pickerInternals/useLocaleDefaults.js.map +1 -1
- package/dist/esm/form/_pickerInternals/useNativeMobilePicker.js +1 -0
- package/dist/esm/form/_pickerInternals/useNativeMobilePicker.js.map +1 -1
- package/dist/esm/form/_pickerInternals/useSegmentedEntry.js +1 -0
- package/dist/esm/form/_pickerInternals/useSegmentedEntry.js.map +1 -1
- package/dist/esm/form/controlLoading.js +1 -0
- package/dist/esm/form/controlLoading.js.map +1 -1
- package/dist/esm/form/groupFieldset.js +1 -0
- package/dist/esm/form/groupFieldset.js.map +1 -1
- package/dist/esm/form/useAutoLabelId.js +1 -0
- package/dist/esm/form/useAutoLabelId.js.map +1 -1
- package/dist/esm/grid/Cell.js +1 -0
- package/dist/esm/grid/Cell.js.map +1 -1
- package/dist/esm/grid/Grid.js +1 -0
- package/dist/esm/grid/Grid.js.map +1 -1
- package/dist/esm/helpers/ClientOnly.js +1 -0
- package/dist/esm/helpers/ClientOnly.js.map +1 -1
- package/dist/esm/helpers/Config.js +1 -0
- package/dist/esm/helpers/Config.js.map +1 -1
- package/dist/esm/helpers/LocaleProvider.js +1 -0
- package/dist/esm/helpers/LocaleProvider.js.map +1 -1
- package/dist/esm/helpers/Theme.js +1 -0
- package/dist/esm/helpers/Theme.js.map +1 -1
- package/dist/esm/helpers/flattenChildren.js +1 -0
- package/dist/esm/helpers/flattenChildren.js.map +1 -1
- package/dist/esm/helpers/inertProps.js +1 -0
- package/dist/esm/helpers/inertProps.js.map +1 -1
- package/dist/esm/helpers/localeFormat.js +1 -0
- package/dist/esm/helpers/localeFormat.js.map +1 -1
- package/dist/esm/helpers/overlayStack.js +1 -0
- package/dist/esm/helpers/overlayStack.js.map +1 -1
- package/dist/esm/helpers/portal.js +1 -0
- package/dist/esm/helpers/portal.js.map +1 -1
- package/dist/esm/helpers/scrollLock.js +1 -0
- package/dist/esm/helpers/scrollLock.js.map +1 -1
- package/dist/esm/helpers/statusRegion.js +1 -0
- package/dist/esm/helpers/statusRegion.js.map +1 -1
- package/dist/esm/helpers/typeahead.js +1 -0
- package/dist/esm/helpers/typeahead.js.map +1 -1
- package/dist/esm/helpers/useAnchoredPosition.js +1 -0
- package/dist/esm/helpers/useAnchoredPosition.js.map +1 -1
- package/dist/esm/helpers/useBulmaClasses.js +1 -0
- package/dist/esm/helpers/useBulmaClasses.js.map +1 -1
- package/dist/esm/helpers/useClientLayoutEffect.js +1 -0
- package/dist/esm/helpers/useClientLayoutEffect.js.map +1 -1
- package/dist/esm/helpers/useColorClasses.js +1 -0
- package/dist/esm/helpers/useColorClasses.js.map +1 -1
- package/dist/esm/helpers/useComboboxEngine.js +1 -0
- package/dist/esm/helpers/useComboboxEngine.js.map +1 -1
- package/dist/esm/helpers/useContextMenuTrigger.js +1 -0
- package/dist/esm/helpers/useContextMenuTrigger.js.map +1 -1
- package/dist/esm/helpers/useFlexboxClasses.js +1 -0
- package/dist/esm/helpers/useFlexboxClasses.js.map +1 -1
- package/dist/esm/helpers/useFocusTrap.js +1 -0
- package/dist/esm/helpers/useFocusTrap.js.map +1 -1
- package/dist/esm/helpers/useHoverOpen.js +1 -0
- package/dist/esm/helpers/useHoverOpen.js.map +1 -1
- package/dist/esm/helpers/useInertSiblings.js +1 -0
- package/dist/esm/helpers/useInertSiblings.js.map +1 -1
- package/dist/esm/helpers/useIsHydrated.js +1 -0
- package/dist/esm/helpers/useIsHydrated.js.map +1 -1
- package/dist/esm/helpers/useLatestRef.js +1 -0
- package/dist/esm/helpers/useLatestRef.js.map +1 -1
- package/dist/esm/helpers/useMenuKeys.js +1 -0
- package/dist/esm/helpers/useMenuKeys.js.map +1 -1
- package/dist/esm/helpers/useOtherClasses.js +1 -0
- package/dist/esm/helpers/useOtherClasses.js.map +1 -1
- package/dist/esm/helpers/useSpacingClasses.js +1 -0
- package/dist/esm/helpers/useSpacingClasses.js.map +1 -1
- package/dist/esm/helpers/useTreeView.js +1 -0
- package/dist/esm/helpers/useTreeView.js.map +1 -1
- package/dist/esm/helpers/useTypographyClasses.js +1 -0
- package/dist/esm/helpers/useTypographyClasses.js.map +1 -1
- package/dist/esm/helpers/useVisibilityClasses.js +1 -0
- package/dist/esm/helpers/useVisibilityClasses.js.map +1 -1
- package/dist/esm/layout/Container.js +1 -0
- package/dist/esm/layout/Container.js.map +1 -1
- package/dist/esm/layout/Footer.js +1 -0
- package/dist/esm/layout/Footer.js.map +1 -1
- package/dist/esm/layout/Hero.js +1 -0
- package/dist/esm/layout/Hero.js.map +1 -1
- package/dist/esm/layout/Level.js +1 -0
- package/dist/esm/layout/Level.js.map +1 -1
- package/dist/esm/layout/Media.js +1 -0
- package/dist/esm/layout/Media.js.map +1 -1
- package/dist/esm/layout/Section.js +1 -0
- package/dist/esm/layout/Section.js.map +1 -1
- package/dist/esm/layout/Splitter.js +1 -0
- package/dist/esm/layout/Splitter.js.map +1 -1
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +300 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +2 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/DataTable.d.ts +1 -1
- package/dist/types/components/useDataTable.d.ts +36 -0
- package/dist/types/form/ColorInput.d.ts +76 -0
- package/dist/types/form/ColorInputBase.d.ts +108 -0
- package/dist/types/form/ColorPicker.d.ts +5 -0
- package/dist/types/form/_colorInternals/EyeDropperButton.d.ts +8 -0
- package/dist/types/form/_colorInternals/pickerState.d.ts +7 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types-cjs/components/DataTable.d.ts +1 -1
- package/dist/types-cjs/components/useDataTable.d.ts +36 -0
- package/dist/types-cjs/form/ColorInput.d.ts +76 -0
- package/dist/types-cjs/form/ColorInputBase.d.ts +108 -0
- package/dist/types-cjs/form/ColorPicker.d.ts +5 -0
- package/dist/types-cjs/form/_colorInternals/EyeDropperButton.d.ts +8 -0
- package/dist/types-cjs/form/_colorInternals/pickerState.d.ts +7 -1
- 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 +2 -1
- package/src/scss/form/_colorinput.scss +109 -0
- package/src/scss/form/_index.scss +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scrollLock.js","sources":["../../../src/helpers/scrollLock.ts"],"sourcesContent":["import { useEffect } from 'react';\n\n// Ref-counted across every caller (e.g. a Dialog rendering its own Modal)\n// so overlapping/nested locks don't fight over which one restores `overflow`.\n// Every overlay that locks body scroll — Modal, Dialog, Sidebar, Loading —\n// goes through here; a component setting `document.body.style.overflow`\n// itself would clear a lock another overlay still needs.\nlet lockCount = 0;\nlet originalOverflow = '';\n\n/**\n * Locks (and ref-counted-ly unlocks) `document.body` scrolling while `active`\n * is true. Safe to call from multiple components at once — the body scroll\n * is only restored once every active caller has released its lock.\n *\n * @function useScrollLock\n * @param active - Whether this caller wants the body scroll locked.\n */\nexport function useScrollLock(active: boolean): void {\n useEffect(() => {\n if (!active) return undefined;\n\n lockCount++;\n if (lockCount === 1) {\n originalOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n\n return () => {\n lockCount--;\n if (lockCount === 0) {\n document.body.style.overflow = originalOverflow;\n }\n };\n }, [active]);\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"scrollLock.js","sources":["../../../src/helpers/scrollLock.ts"],"sourcesContent":["import { useEffect } from 'react';\n\n// Ref-counted across every caller (e.g. a Dialog rendering its own Modal)\n// so overlapping/nested locks don't fight over which one restores `overflow`.\n// Every overlay that locks body scroll — Modal, Dialog, Sidebar, Loading —\n// goes through here; a component setting `document.body.style.overflow`\n// itself would clear a lock another overlay still needs.\nlet lockCount = 0;\nlet originalOverflow = '';\n\n/**\n * Locks (and ref-counted-ly unlocks) `document.body` scrolling while `active`\n * is true. Safe to call from multiple components at once — the body scroll\n * is only restored once every active caller has released its lock.\n *\n * @function useScrollLock\n * @param active - Whether this caller wants the body scroll locked.\n */\nexport function useScrollLock(active: boolean): void {\n useEffect(() => {\n if (!active) return undefined;\n\n lockCount++;\n if (lockCount === 1) {\n originalOverflow = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n\n return () => {\n lockCount--;\n if (lockCount === 0) {\n document.body.style.overflow = originalOverflow;\n }\n };\n }, [active]);\n}\n"],"names":[],"mappings":";;;;AAEA;AACA;AACA;AACA;AACA;AACA,IAAI,SAAS,GAAG,CAAC;AACjB,IAAI,gBAAgB,GAAG,EAAE;AAEzB;;;;;;;AAOG;AACG,SAAU,aAAa,CAAC,MAAe,EAAA;IAC3C,SAAS,CAAC,MAAK;AACb,QAAA,IAAI,CAAC,MAAM;AAAE,YAAA,OAAO,SAAS;AAE7B,QAAA,SAAS,EAAE;AACX,QAAA,IAAI,SAAS,KAAK,CAAC,EAAE;YACnB,gBAAgB,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ;YAC/C,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,QAAQ;QACzC;AAEA,QAAA,OAAO,MAAK;AACV,YAAA,SAAS,EAAE;AACX,YAAA,IAAI,SAAS,KAAK,CAAC,EAAE;gBACnB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,gBAAgB;YACjD;AACF,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;AACd;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statusRegion.js","sources":["../../../src/helpers/statusRegion.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react';\n\n/**\n * What a status region says for one of a container's items.\n */\ninterface Announcement {\n /** The id of the item it announces. */\n id: string;\n /** The text a screen reader reads out. */\n text: string;\n}\n\n/**\n * An announcement in the region.\n */\ninterface WrittenAnnouncement extends Announcement {\n /** The batch it was written with, which is what its clear removes. */\n batch: number;\n}\n\n/**\n * How long after an item appears its announcement is written, in ms. The wait\n * lets a screen reader register a region the container has only just mounted,\n * and items shown in quick succession are written, and read out, together.\n */\nexport const announceDelay = 100;\n\n/**\n * How long an announcement stays in the region once written, in ms. That's\n * long enough for a screen reader to pick it up. Clearing it after keeps a\n * second copy of the item's text from sitting in the page, where someone\n * reading through the page, or a test looking the text up, would find it\n * again.\n */\nexport const announcementLifetime = 1000;\n\n/**\n * What the region says, and what it's about to say.\n */\ninterface Announcements {\n /**\n * Changes whenever announcements join the wait, which restarts it. It also\n * names the batch they're written with.\n */\n key: number;\n /** Waiting to be written, in the order their items were shown. */\n waiting: Announcement[];\n /** In the region, in the order they were written. */\n written: WrittenAnnouncement[];\n}\n\nconst noAnnouncements: Announcement[] = [];\nconst nothingWritten: WrittenAnnouncement[] = [];\nconst noAnnouncementsYet: Announcements = {\n key: 0,\n waiting: noAnnouncements,\n written: nothingWritten,\n};\n\n// Hides the region with inline styles rather than a class, so it needs no\n// stylesheet, including the helper classes a modular Bulma build can leave\n// out. Clipping it, rather than using `display` or `visibility`, keeps it in\n// the accessibility tree. The declarations match the `extras-sr-only` mixin,\n// plus `clip-path`, which replaces the deprecated `clip`. The negative margin\n// pulls the 1px box back inside the page, so it doesn't add scrollable\n// overflow. Exported for other live regions that must work without a\n// stylesheet, such as a range calendar's.\nexport const visuallyHidden: React.CSSProperties = {\n position: 'absolute',\n width: '1px',\n height: '1px',\n padding: 0,\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0, 0, 0, 0)',\n clipPath: 'inset(50%)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * Works out what a status region says. Each item is announced once each time\n * it appears, so one that goes and comes back is announced again. Its\n * announcement is written `announceDelay` after it appears, or after the last\n * item to join it when several appear in quick succession, alongside whatever\n * the region still says. Once written, it stays for `announcementLifetime`,\n * whatever is written after it and whether or not its item goes in the\n * meantime. An item that goes before its announcement is written isn't\n * announced.\n *\n * Nothing is written in the commit that mounts the region or the item. A\n * screen reader reliably announces a polite live region whose content changes\n * after the region is already in the page, and not always one that arrives\n * with its content. By the time `describe` runs, an item's rendered text can\n * be read.\n *\n * @function useAnnouncements\n * @param items - The items to announce, in the order they were shown.\n * @param describe - The text to announce for an item, or nothing to skip it.\n * It runs once the item is on screen, and it must keep its identity between\n * renders (a module-level function or a `useCallback`), or every render\n * re-checks the items.\n * @returns The announcements to render, in the order they were written.\n */\nfunction useAnnouncements<Item extends { id: string }>(\n items: readonly Item[],\n describe: (item: Item) => string | null\n): WrittenAnnouncement[] {\n // The ids of the items on screen as of the last change, every one of which\n // has been looked at for an announcement already.\n const onScreenIdsRef = useRef(new Set<string>());\n // Clears that haven't run yet. They outlive the batch that started them, so\n // they're only stopped when the region unmounts.\n const clearTimersRef = useRef(new Set<ReturnType<typeof setTimeout>>());\n const [announcements, setAnnouncements] = useState(noAnnouncementsYet);\n\n // Runs after the commit that puts an item on screen, which is the first\n // point its rendered text can be read.\n useEffect(() => {\n const announced = onScreenIdsRef.current;\n const appeared = items.filter(item => !announced.has(item.id));\n // Items that have gone drop out here too, so one that comes back has\n // appeared again.\n onScreenIdsRef.current = new Set(items.map(item => item.id));\n\n const next: Announcement[] = [];\n for (const item of appeared) {\n const text = describe(item);\n if (text) next.push({ id: item.id, text });\n }\n if (next.length > 0) {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- an item's rendered text can only be read once it's on screen\n setAnnouncements(current => ({\n ...current,\n key: current.key + 1,\n // Announcements still waiting go out with the new ones, apart from\n // one whose item came back before it was written, which the new one\n // replaces. What's already written stays put.\n waiting: [\n ...current.waiting.filter(\n waiting => !next.some(({ id }) => id === waiting.id)\n ),\n ...next,\n ],\n }));\n }\n }, [items, describe]);\n\n // The write only touches the batch it was started for: a newer batch can\n // be set before the render that cancels this timer. Its clear is started\n // by the write itself, so it's timed from the write without waiting on a\n // render, and a newer batch arriving doesn't cut short what's written.\n const { key } = announcements;\n useEffect(() => {\n if (key === noAnnouncementsYet.key) return undefined;\n const clearTimers = clearTimersRef.current;\n const write = setTimeout(() => {\n // Whether an item is on screen is decided here, once. An item that\n // went before its announcement was due isn't announced, and one that\n // goes after keeps its announcement for the rest of its lifetime, when\n // the region holds the only copy of its text rather than a second one.\n const onScreen = onScreenIdsRef.current;\n setAnnouncements(current =>\n current.key === key\n ? {\n ...current,\n waiting: noAnnouncements,\n written: [\n ...current.written,\n ...current.waiting\n .filter(announcement => onScreen.has(announcement.id))\n .map(announcement => ({ ...announcement, batch: key })),\n ],\n }\n : current\n );\n // eslint-disable-next-line @eslint-react/web-api-no-leaked-timeout -- it has to outlive this effect when a newer batch arrives, so the unmount effect below clears it\n const clear = setTimeout(() => {\n clearTimers.delete(clear);\n setAnnouncements(current => {\n const written = current.written.filter(\n announcement => announcement.batch !== key\n );\n return written.length === current.written.length\n ? current\n : { ...current, written };\n });\n }, announcementLifetime);\n clearTimers.add(clear);\n }, announceDelay);\n return () => clearTimeout(write);\n }, [key]);\n\n useEffect(() => {\n const clearTimers = clearTimersRef.current;\n return () => {\n clearTimers.forEach(clearTimeout);\n clearTimers.clear();\n };\n }, []);\n\n return announcements.written;\n}\n\n/**\n * Reads an element out roughly the way a screen reader would: its text, with\n * an element's `aria-label`, or an image's `alt`, standing in for what's\n * inside it, a break between block-level elements, and parts that are\n * `aria-hidden`, `hidden` or inline-styled `display: none` left out. It isn't\n * the full accessible name computation, so `aria-labelledby`, CSS-generated\n * content, content hidden by a stylesheet and other ways of naming or hiding\n * content aren't followed.\n *\n * @function spokenText\n * @param element - The element to read.\n * @returns Its text, with runs of whitespace collapsed and the ends trimmed.\n */\nexport function spokenText(element: Element): string {\n return readAloud(element).replace(/\\s+/g, ' ').trim();\n}\n\n// Elements a browser displays as blocks by default, and `br`. A screen reader\n// pauses between them, so their text shouldn't run into the text beside them.\n// They're known by tag rather than by computed `display`, which would depend\n// on a stylesheet and on layout.\nconst blockLevelTags = new Set([\n 'ADDRESS',\n 'ARTICLE',\n 'ASIDE',\n 'BLOCKQUOTE',\n 'BR',\n 'CAPTION',\n 'DD',\n 'DETAILS',\n 'DIALOG',\n 'DIV',\n 'DL',\n 'DT',\n 'FIELDSET',\n 'FIGCAPTION',\n 'FIGURE',\n 'FOOTER',\n 'FORM',\n 'H1',\n 'H2',\n 'H3',\n 'H4',\n 'H5',\n 'H6',\n 'HEADER',\n 'HGROUP',\n 'HR',\n 'LEGEND',\n 'LI',\n 'MAIN',\n 'MENU',\n 'NAV',\n 'OL',\n 'P',\n 'PRE',\n 'SEARCH',\n 'SECTION',\n 'SUMMARY',\n 'TABLE',\n 'TBODY',\n 'TD',\n 'TFOOT',\n 'TH',\n 'THEAD',\n 'TR',\n 'UL',\n]);\n\n/**\n * Whether an element is left out of what a screen reader reads.\n *\n * @function isHidden\n * @param element - The element to check.\n * @returns True for `aria-hidden=\"true\"`, the `hidden` attribute, and an\n * inline `display: none`, on an HTML, SVG or any other element with an\n * inline style.\n */\nconst isHidden = (element: Element): boolean =>\n element.getAttribute('aria-hidden') === 'true' ||\n element.hasAttribute('hidden') ||\n (element as Partial<ElementCSSInlineStyle>).style?.display === 'none';\n\n/**\n * The text `spokenText` reads for an element, before its whitespace is\n * tidied.\n *\n * @function readAloud\n * @param element - The element to read.\n * @returns Its text.\n */\nfunction readAloud(element: Element): string {\n if (isHidden(element)) return '';\n const name =\n element.getAttribute('aria-label') ||\n (element.tagName === 'IMG' ? element.getAttribute('alt') : null);\n const text =\n name ||\n Array.from(element.childNodes, child => {\n if (child instanceof Element) return readAloud(child);\n return child.nodeType === Node.TEXT_NODE ? child.textContent : '';\n }).join('');\n // A name stands in for the element's content, and a block's content stands\n // apart from what's around it, so both are padded to keep them from running\n // into the neighboring text. An inline run, like `<span>Up</span>` then\n // `<span>loaded</span>`, still reads as one word.\n return name || blockLevelTags.has(element.tagName) ? ` ${text} ` : text;\n}\n\n/**\n * Props for StatusRegion.\n */\nexport interface StatusRegionProps<Item extends { id: string }> {\n /** The items to announce politely, in the order they were shown. */\n items: readonly Item[];\n /**\n * The text to announce for an item, or nothing to skip it, as\n * `useAnnouncements` takes it.\n */\n describe: (item: Item) => string | null;\n}\n\n/**\n * A visually hidden, polite `status` live region that a container announces\n * its items through. The container renders it from the moment it mounts,\n * whether or not it has anything to show, so the region is already in the\n * page when an announcement is written into it.\n *\n * The announcements are this component's own state, so writing and clearing\n * them re-renders the region and leaves the container's items alone. Each one\n * is its own keyed node, so a re-render that leaves the announcements alone\n * leaves the region's content alone too, and nothing is read out twice.\n *\n * @function StatusRegion\n * @param props - The items to announce, and the text to announce for each.\n * @returns The region.\n */\nexport function StatusRegion<Item extends { id: string }>({\n items,\n describe,\n}: StatusRegionProps<Item>): React.ReactElement {\n const announcements = useAnnouncements(items, describe);\n\n // `role=\"status\"` makes a region atomic unless it says otherwise, which\n // reads the whole region on every change. Each announcement is its own\n // node, and only additions are relevant by default, so with atomic off a\n // screen reader reads just what was written, and removing one announcement\n // doesn't read out the ones still there. A node is keyed by its batch as\n // well as its item, since an item that comes back can be written again\n // while what it said before is still there.\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"false\"\n style={visuallyHidden}\n >\n {announcements.map(announcement => (\n <div key={`${announcement.batch}:${announcement.id}`}>\n {announcement.text}\n </div>\n ))}\n </div>\n );\n}\n"],"names":["noAnnouncements","_jsx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"statusRegion.js","sources":["../../../src/helpers/statusRegion.tsx"],"sourcesContent":["import React, { useEffect, useRef, useState } from 'react';\n\n/**\n * What a status region says for one of a container's items.\n */\ninterface Announcement {\n /** The id of the item it announces. */\n id: string;\n /** The text a screen reader reads out. */\n text: string;\n}\n\n/**\n * An announcement in the region.\n */\ninterface WrittenAnnouncement extends Announcement {\n /** The batch it was written with, which is what its clear removes. */\n batch: number;\n}\n\n/**\n * How long after an item appears its announcement is written, in ms. The wait\n * lets a screen reader register a region the container has only just mounted,\n * and items shown in quick succession are written, and read out, together.\n */\nexport const announceDelay = 100;\n\n/**\n * How long an announcement stays in the region once written, in ms. That's\n * long enough for a screen reader to pick it up. Clearing it after keeps a\n * second copy of the item's text from sitting in the page, where someone\n * reading through the page, or a test looking the text up, would find it\n * again.\n */\nexport const announcementLifetime = 1000;\n\n/**\n * What the region says, and what it's about to say.\n */\ninterface Announcements {\n /**\n * Changes whenever announcements join the wait, which restarts it. It also\n * names the batch they're written with.\n */\n key: number;\n /** Waiting to be written, in the order their items were shown. */\n waiting: Announcement[];\n /** In the region, in the order they were written. */\n written: WrittenAnnouncement[];\n}\n\nconst noAnnouncements: Announcement[] = [];\nconst nothingWritten: WrittenAnnouncement[] = [];\nconst noAnnouncementsYet: Announcements = {\n key: 0,\n waiting: noAnnouncements,\n written: nothingWritten,\n};\n\n// Hides the region with inline styles rather than a class, so it needs no\n// stylesheet, including the helper classes a modular Bulma build can leave\n// out. Clipping it, rather than using `display` or `visibility`, keeps it in\n// the accessibility tree. The declarations match the `extras-sr-only` mixin,\n// plus `clip-path`, which replaces the deprecated `clip`. The negative margin\n// pulls the 1px box back inside the page, so it doesn't add scrollable\n// overflow. Exported for other live regions that must work without a\n// stylesheet, such as a range calendar's.\nexport const visuallyHidden: React.CSSProperties = {\n position: 'absolute',\n width: '1px',\n height: '1px',\n padding: 0,\n margin: '-1px',\n overflow: 'hidden',\n clip: 'rect(0, 0, 0, 0)',\n clipPath: 'inset(50%)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * Works out what a status region says. Each item is announced once each time\n * it appears, so one that goes and comes back is announced again. Its\n * announcement is written `announceDelay` after it appears, or after the last\n * item to join it when several appear in quick succession, alongside whatever\n * the region still says. Once written, it stays for `announcementLifetime`,\n * whatever is written after it and whether or not its item goes in the\n * meantime. An item that goes before its announcement is written isn't\n * announced.\n *\n * Nothing is written in the commit that mounts the region or the item. A\n * screen reader reliably announces a polite live region whose content changes\n * after the region is already in the page, and not always one that arrives\n * with its content. By the time `describe` runs, an item's rendered text can\n * be read.\n *\n * @function useAnnouncements\n * @param items - The items to announce, in the order they were shown.\n * @param describe - The text to announce for an item, or nothing to skip it.\n * It runs once the item is on screen, and it must keep its identity between\n * renders (a module-level function or a `useCallback`), or every render\n * re-checks the items.\n * @returns The announcements to render, in the order they were written.\n */\nfunction useAnnouncements<Item extends { id: string }>(\n items: readonly Item[],\n describe: (item: Item) => string | null\n): WrittenAnnouncement[] {\n // The ids of the items on screen as of the last change, every one of which\n // has been looked at for an announcement already.\n const onScreenIdsRef = useRef(new Set<string>());\n // Clears that haven't run yet. They outlive the batch that started them, so\n // they're only stopped when the region unmounts.\n const clearTimersRef = useRef(new Set<ReturnType<typeof setTimeout>>());\n const [announcements, setAnnouncements] = useState(noAnnouncementsYet);\n\n // Runs after the commit that puts an item on screen, which is the first\n // point its rendered text can be read.\n useEffect(() => {\n const announced = onScreenIdsRef.current;\n const appeared = items.filter(item => !announced.has(item.id));\n // Items that have gone drop out here too, so one that comes back has\n // appeared again.\n onScreenIdsRef.current = new Set(items.map(item => item.id));\n\n const next: Announcement[] = [];\n for (const item of appeared) {\n const text = describe(item);\n if (text) next.push({ id: item.id, text });\n }\n if (next.length > 0) {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- an item's rendered text can only be read once it's on screen\n setAnnouncements(current => ({\n ...current,\n key: current.key + 1,\n // Announcements still waiting go out with the new ones, apart from\n // one whose item came back before it was written, which the new one\n // replaces. What's already written stays put.\n waiting: [\n ...current.waiting.filter(\n waiting => !next.some(({ id }) => id === waiting.id)\n ),\n ...next,\n ],\n }));\n }\n }, [items, describe]);\n\n // The write only touches the batch it was started for: a newer batch can\n // be set before the render that cancels this timer. Its clear is started\n // by the write itself, so it's timed from the write without waiting on a\n // render, and a newer batch arriving doesn't cut short what's written.\n const { key } = announcements;\n useEffect(() => {\n if (key === noAnnouncementsYet.key) return undefined;\n const clearTimers = clearTimersRef.current;\n const write = setTimeout(() => {\n // Whether an item is on screen is decided here, once. An item that\n // went before its announcement was due isn't announced, and one that\n // goes after keeps its announcement for the rest of its lifetime, when\n // the region holds the only copy of its text rather than a second one.\n const onScreen = onScreenIdsRef.current;\n setAnnouncements(current =>\n current.key === key\n ? {\n ...current,\n waiting: noAnnouncements,\n written: [\n ...current.written,\n ...current.waiting\n .filter(announcement => onScreen.has(announcement.id))\n .map(announcement => ({ ...announcement, batch: key })),\n ],\n }\n : current\n );\n // eslint-disable-next-line @eslint-react/web-api-no-leaked-timeout -- it has to outlive this effect when a newer batch arrives, so the unmount effect below clears it\n const clear = setTimeout(() => {\n clearTimers.delete(clear);\n setAnnouncements(current => {\n const written = current.written.filter(\n announcement => announcement.batch !== key\n );\n return written.length === current.written.length\n ? current\n : { ...current, written };\n });\n }, announcementLifetime);\n clearTimers.add(clear);\n }, announceDelay);\n return () => clearTimeout(write);\n }, [key]);\n\n useEffect(() => {\n const clearTimers = clearTimersRef.current;\n return () => {\n clearTimers.forEach(clearTimeout);\n clearTimers.clear();\n };\n }, []);\n\n return announcements.written;\n}\n\n/**\n * Reads an element out roughly the way a screen reader would: its text, with\n * an element's `aria-label`, or an image's `alt`, standing in for what's\n * inside it, a break between block-level elements, and parts that are\n * `aria-hidden`, `hidden` or inline-styled `display: none` left out. It isn't\n * the full accessible name computation, so `aria-labelledby`, CSS-generated\n * content, content hidden by a stylesheet and other ways of naming or hiding\n * content aren't followed.\n *\n * @function spokenText\n * @param element - The element to read.\n * @returns Its text, with runs of whitespace collapsed and the ends trimmed.\n */\nexport function spokenText(element: Element): string {\n return readAloud(element).replace(/\\s+/g, ' ').trim();\n}\n\n// Elements a browser displays as blocks by default, and `br`. A screen reader\n// pauses between them, so their text shouldn't run into the text beside them.\n// They're known by tag rather than by computed `display`, which would depend\n// on a stylesheet and on layout.\nconst blockLevelTags = new Set([\n 'ADDRESS',\n 'ARTICLE',\n 'ASIDE',\n 'BLOCKQUOTE',\n 'BR',\n 'CAPTION',\n 'DD',\n 'DETAILS',\n 'DIALOG',\n 'DIV',\n 'DL',\n 'DT',\n 'FIELDSET',\n 'FIGCAPTION',\n 'FIGURE',\n 'FOOTER',\n 'FORM',\n 'H1',\n 'H2',\n 'H3',\n 'H4',\n 'H5',\n 'H6',\n 'HEADER',\n 'HGROUP',\n 'HR',\n 'LEGEND',\n 'LI',\n 'MAIN',\n 'MENU',\n 'NAV',\n 'OL',\n 'P',\n 'PRE',\n 'SEARCH',\n 'SECTION',\n 'SUMMARY',\n 'TABLE',\n 'TBODY',\n 'TD',\n 'TFOOT',\n 'TH',\n 'THEAD',\n 'TR',\n 'UL',\n]);\n\n/**\n * Whether an element is left out of what a screen reader reads.\n *\n * @function isHidden\n * @param element - The element to check.\n * @returns True for `aria-hidden=\"true\"`, the `hidden` attribute, and an\n * inline `display: none`, on an HTML, SVG or any other element with an\n * inline style.\n */\nconst isHidden = (element: Element): boolean =>\n element.getAttribute('aria-hidden') === 'true' ||\n element.hasAttribute('hidden') ||\n (element as Partial<ElementCSSInlineStyle>).style?.display === 'none';\n\n/**\n * The text `spokenText` reads for an element, before its whitespace is\n * tidied.\n *\n * @function readAloud\n * @param element - The element to read.\n * @returns Its text.\n */\nfunction readAloud(element: Element): string {\n if (isHidden(element)) return '';\n const name =\n element.getAttribute('aria-label') ||\n (element.tagName === 'IMG' ? element.getAttribute('alt') : null);\n const text =\n name ||\n Array.from(element.childNodes, child => {\n if (child instanceof Element) return readAloud(child);\n return child.nodeType === Node.TEXT_NODE ? child.textContent : '';\n }).join('');\n // A name stands in for the element's content, and a block's content stands\n // apart from what's around it, so both are padded to keep them from running\n // into the neighboring text. An inline run, like `<span>Up</span>` then\n // `<span>loaded</span>`, still reads as one word.\n return name || blockLevelTags.has(element.tagName) ? ` ${text} ` : text;\n}\n\n/**\n * Props for StatusRegion.\n */\nexport interface StatusRegionProps<Item extends { id: string }> {\n /** The items to announce politely, in the order they were shown. */\n items: readonly Item[];\n /**\n * The text to announce for an item, or nothing to skip it, as\n * `useAnnouncements` takes it.\n */\n describe: (item: Item) => string | null;\n}\n\n/**\n * A visually hidden, polite `status` live region that a container announces\n * its items through. The container renders it from the moment it mounts,\n * whether or not it has anything to show, so the region is already in the\n * page when an announcement is written into it.\n *\n * The announcements are this component's own state, so writing and clearing\n * them re-renders the region and leaves the container's items alone. Each one\n * is its own keyed node, so a re-render that leaves the announcements alone\n * leaves the region's content alone too, and nothing is read out twice.\n *\n * @function StatusRegion\n * @param props - The items to announce, and the text to announce for each.\n * @returns The region.\n */\nexport function StatusRegion<Item extends { id: string }>({\n items,\n describe,\n}: StatusRegionProps<Item>): React.ReactElement {\n const announcements = useAnnouncements(items, describe);\n\n // `role=\"status\"` makes a region atomic unless it says otherwise, which\n // reads the whole region on every change. Each announcement is its own\n // node, and only additions are relevant by default, so with atomic off a\n // screen reader reads just what was written, and removing one announcement\n // doesn't read out the ones still there. A node is keyed by its batch as\n // well as its item, since an item that comes back can be written again\n // while what it said before is still there.\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"false\"\n style={visuallyHidden}\n >\n {announcements.map(announcement => (\n <div key={`${announcement.batch}:${announcement.id}`}>\n {announcement.text}\n </div>\n ))}\n </div>\n );\n}\n"],"names":["noAnnouncements","_jsx"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typeahead.js","sources":["../../../src/helpers/typeahead.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useEffect, useMemo, useRef } from 'react';\n\n/** How long typed characters keep adding up to one search, in milliseconds. */\nexport const TYPEAHEAD_TIMEOUT = 500;\n\n/**\n * The text typing finds an element by: what it says, without what's hidden\n * from assistive technology, such as an icon whose glyph is a ligature's name.\n */\nexport function typeaheadText(element: Element): string {\n let text = '';\n const read = (node: Node) => {\n node.childNodes.forEach(child => {\n if (child.nodeType === 3) text += child.textContent;\n else if (\n child.nodeType === 1 &&\n (child as Element).getAttribute('aria-hidden') !== 'true'\n ) {\n read(child);\n }\n });\n };\n read(element);\n return text.trim().toLowerCase();\n}\n\n/**\n * The item typing `search` moves to, or none. A single character looks from\n * the item after the focused one, so typing the same letter again cycles\n * through the items that start with it, and so does a run of one letter\n * (\"dd\"). A longer search looks from the focused item, so typing more of\n * its own text keeps it.\n *\n * @param items - The items typing can reach, in the order they're shown.\n * @param search - What's been typed, lowercased.\n * @param current - The focused item's index in `items`, or -1 for none.\n * @param textOf - The text an item is found by. By default it's the item's\n * own, read by `typeaheadText`.\n */\nexport function typeaheadMatch<T extends Element>(\n items: T[],\n search: string,\n current: number,\n textOf: (item: T) => string = typeaheadText\n): T | undefined {\n const repeated = Array.from(search).every(char => char === search[0]);\n const wanted = repeated ? search[0] : search;\n const start = current < 0 ? 0 : current + (wanted.length === 1 ? 1 : 0);\n for (let i = 0; i < items.length; i += 1) {\n const item = items[(start + i) % items.length];\n if (textOf(item).startsWith(wanted)) return item;\n }\n return undefined;\n}\n\n/** What `useTypeahead` returns. */\nexport interface Typeahead {\n /**\n * Adds a typed character to the search and returns the whole search,\n * lowercased. Characters typed within `TYPEAHEAD_TIMEOUT` of each other\n * add up, and after that the next one starts a new search.\n */\n type: (char: string) => string;\n /** Forgets what has been typed, so the next character starts a new search. */\n reset: () => void;\n}\n\n/**\n * Keeps what has been typed for a type-ahead search, and forgets it once\n * `TYPEAHEAD_TIMEOUT` passes with nothing typed. Pair it with\n * `typeaheadMatch` to find the item the search names.\n *\n * @function useTypeahead\n * @returns A way to add a character to the search, and a way to forget it.\n */\nexport function useTypeahead(): Typeahead {\n const typedRef = useRef<{\n text: string;\n timer?: ReturnType<typeof setTimeout>;\n }>({ text: '' });\n\n useEffect(() => {\n const typed = typedRef.current;\n return () => clearTimeout(typed.timer);\n }, []);\n\n return useMemo<Typeahead>(() => {\n const reset = () => {\n const typed = typedRef.current;\n clearTimeout(typed.timer);\n typed.text = '';\n };\n const type = (char: string) => {\n const typed = typedRef.current;\n typed.text += char.toLowerCase();\n clearTimeout(typed.timer);\n typed.timer = setTimeout(reset, TYPEAHEAD_TIMEOUT);\n return typed.text;\n };\n return { type, reset };\n }, []);\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"typeahead.js","sources":["../../../src/helpers/typeahead.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useEffect, useMemo, useRef } from 'react';\n\n/** How long typed characters keep adding up to one search, in milliseconds. */\nexport const TYPEAHEAD_TIMEOUT = 500;\n\n/**\n * The text typing finds an element by: what it says, without what's hidden\n * from assistive technology, such as an icon whose glyph is a ligature's name.\n */\nexport function typeaheadText(element: Element): string {\n let text = '';\n const read = (node: Node) => {\n node.childNodes.forEach(child => {\n if (child.nodeType === 3) text += child.textContent;\n else if (\n child.nodeType === 1 &&\n (child as Element).getAttribute('aria-hidden') !== 'true'\n ) {\n read(child);\n }\n });\n };\n read(element);\n return text.trim().toLowerCase();\n}\n\n/**\n * The item typing `search` moves to, or none. A single character looks from\n * the item after the focused one, so typing the same letter again cycles\n * through the items that start with it, and so does a run of one letter\n * (\"dd\"). A longer search looks from the focused item, so typing more of\n * its own text keeps it.\n *\n * @param items - The items typing can reach, in the order they're shown.\n * @param search - What's been typed, lowercased.\n * @param current - The focused item's index in `items`, or -1 for none.\n * @param textOf - The text an item is found by. By default it's the item's\n * own, read by `typeaheadText`.\n */\nexport function typeaheadMatch<T extends Element>(\n items: T[],\n search: string,\n current: number,\n textOf: (item: T) => string = typeaheadText\n): T | undefined {\n const repeated = Array.from(search).every(char => char === search[0]);\n const wanted = repeated ? search[0] : search;\n const start = current < 0 ? 0 : current + (wanted.length === 1 ? 1 : 0);\n for (let i = 0; i < items.length; i += 1) {\n const item = items[(start + i) % items.length];\n if (textOf(item).startsWith(wanted)) return item;\n }\n return undefined;\n}\n\n/** What `useTypeahead` returns. */\nexport interface Typeahead {\n /**\n * Adds a typed character to the search and returns the whole search,\n * lowercased. Characters typed within `TYPEAHEAD_TIMEOUT` of each other\n * add up, and after that the next one starts a new search.\n */\n type: (char: string) => string;\n /** Forgets what has been typed, so the next character starts a new search. */\n reset: () => void;\n}\n\n/**\n * Keeps what has been typed for a type-ahead search, and forgets it once\n * `TYPEAHEAD_TIMEOUT` passes with nothing typed. Pair it with\n * `typeaheadMatch` to find the item the search names.\n *\n * @function useTypeahead\n * @returns A way to add a character to the search, and a way to forget it.\n */\nexport function useTypeahead(): Typeahead {\n const typedRef = useRef<{\n text: string;\n timer?: ReturnType<typeof setTimeout>;\n }>({ text: '' });\n\n useEffect(() => {\n const typed = typedRef.current;\n return () => clearTimeout(typed.timer);\n }, []);\n\n return useMemo<Typeahead>(() => {\n const reset = () => {\n const typed = typedRef.current;\n clearTimeout(typed.timer);\n typed.text = '';\n };\n const type = (char: string) => {\n const typed = typedRef.current;\n typed.text += char.toLowerCase();\n clearTimeout(typed.timer);\n typed.timer = setTimeout(reset, TYPEAHEAD_TIMEOUT);\n return typed.text;\n };\n return { type, reset };\n }, []);\n}\n"],"names":[],"mappings":";;;;AAAA;AAGA;AACO,MAAM,iBAAiB,GAAG;AAEjC;;;AAGG;AACG,SAAU,aAAa,CAAC,OAAgB,EAAA;IAC5C,IAAI,IAAI,GAAG,EAAE;AACb,IAAA,MAAM,IAAI,GAAG,CAAC,IAAU,KAAI;AAC1B,QAAA,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,IAAG;AAC9B,YAAA,IAAI,KAAK,CAAC,QAAQ,KAAK,CAAC;AAAE,gBAAA,IAAI,IAAI,KAAK,CAAC,WAAW;AAC9C,iBAAA,IACH,KAAK,CAAC,QAAQ,KAAK,CAAC;gBACnB,KAAiB,CAAC,YAAY,CAAC,aAAa,CAAC,KAAK,MAAM,EACzD;gBACA,IAAI,CAAC,KAAK,CAAC;YACb;AACF,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC;IACD,IAAI,CAAC,OAAO,CAAC;AACb,IAAA,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE;AAClC;AAEA;;;;;;;;;;;;AAYG;AACG,SAAU,cAAc,CAC5B,KAAU,EACV,MAAc,EACd,OAAe,EACf,MAAA,GAA8B,aAAa,EAAA;IAE3C,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC;AACrE,IAAA,MAAM,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM;AAC5C,IAAA,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,GAAG,CAAC,GAAG,OAAO,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACvE,IAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE;AACxC,QAAA,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC;QAC9C,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;AAAE,YAAA,OAAO,IAAI;IAClD;AACA,IAAA,OAAO,SAAS;AAClB;AAcA;;;;;;;AAOG;SACa,YAAY,GAAA;IAC1B,MAAM,QAAQ,GAAG,MAAM,CAGpB,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAEhB,SAAS,CAAC,MAAK;AACb,QAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO;QAC9B,OAAO,MAAM,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC;IACxC,CAAC,EAAE,EAAE,CAAC;IAEN,OAAO,OAAO,CAAY,MAAK;QAC7B,MAAM,KAAK,GAAG,MAAK;AACjB,YAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO;AAC9B,YAAA,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC;AACzB,YAAA,KAAK,CAAC,IAAI,GAAG,EAAE;AACjB,QAAA,CAAC;AACD,QAAA,MAAM,IAAI,GAAG,CAAC,IAAY,KAAI;AAC5B,YAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO;AAC9B,YAAA,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,WAAW,EAAE;AAChC,YAAA,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC;YACzB,KAAK,CAAC,KAAK,GAAG,UAAU,CAAC,KAAK,EAAE,iBAAiB,CAAC;YAClD,OAAO,KAAK,CAAC,IAAI;AACnB,QAAA,CAAC;AACD,QAAA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE;IACxB,CAAC,EAAE,EAAE,CAAC;AACR;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAnchoredPosition.js","sources":["../../../src/helpers/useAnchoredPosition.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useRef, useState } from 'react';\nimport type { RefObject } from 'react';\nimport type { PickerPosition } from '../form/_pickerInternals/pickerTypes';\nimport { useClientLayoutEffect } from './useClientLayoutEffect';\n\n/** A corner of the anchor the panel opens from, once `auto` is resolved. */\nexport type AnchoredCorner = Exclude<PickerPosition, 'auto'>;\n\n/**\n * Where the panel goes: the corner it opens from and, when it is fixed to\n * the viewport, its coordinates there.\n */\nexport interface AnchoredPosition {\n position: AnchoredCorner;\n top?: number;\n left?: number;\n /**\n * With `matchWidth`, the anchor's width, which a fixed panel takes as its\n * `min-width`.\n */\n minWidth?: number;\n}\n\n/**\n * Options for `useAnchoredPosition`.\n */\nexport interface UseAnchoredPositionOptions {\n /** Whether the panel is on the page. Nothing is measured while it is not. */\n active: boolean;\n /** The corner asked for, or `auto` to pick one that keeps the panel in the viewport. */\n position: PickerPosition;\n /**\n * Whether the panel is fixed to the viewport (it was portaled away from the\n * anchor), which needs coordinates. Otherwise the panel's CSS places it\n * against the anchor from the corner alone.\n */\n fixed: boolean;\n /**\n * The gap between the anchor and the panel, in pixels, that the hook puts\n * there itself: added to the fixed coordinates and to the room `auto` asks\n * for below the anchor. Popover and the pickers' popover pass `0` and take\n * their gap from a margin in their stylesheet, read through `styledGap`, so\n * a theme's offset moves the panel and `auto` alike. A new panel should do\n * the same rather than rely on this default, which no stylesheet sees.\n * @defaultValue 4\n */\n offset?: number;\n /**\n * Reads the gap the panel's own stylesheet puts between it and the anchor,\n * in pixels, each time the panel is measured. `auto` leaves room for it as\n * well; the coordinates don't include it, because the stylesheet applies it.\n */\n styledGap?: (panel: HTMLElement) => number;\n /**\n * Whether the panel is at least as wide as the anchor, as a list under its\n * field is. A panel in place gets that from its stylesheet. A fixed one\n * can't, so its placement carries the anchor's width as `minWidth` for the\n * panel to set, and its coordinates and `auto` already allow for it.\n * @defaultValue false\n */\n matchWidth?: boolean;\n /**\n * Measures again after each render of the component holding the hook, for\n * a panel whose content changes while it's open, as a filtered list's\n * does. A fixed panel above its anchor is placed from its height, and\n * `auto` reads the height too, so a measurement taken when it opened goes\n * stale as the content changes.\n * @defaultValue false\n */\n followContent?: boolean;\n}\n\n/**\n * A `styledGap` for a panel whose stylesheet puts the gap in a margin on the\n * side facing the anchor, negative on a portaled panel above it, so the\n * larger magnitude is the gap. In pixels.\n */\nexport function marginGap(panel: HTMLElement): number {\n const { marginTop, marginBottom } = getComputedStyle(panel);\n return Math.max(\n Math.abs(parseFloat(marginTop)) || 0,\n Math.abs(parseFloat(marginBottom)) || 0\n );\n}\n\n/**\n * The corner that keeps a panel of this size in the viewport, preferring\n * below the anchor and lined up with its left edge.\n */\nfunction resolveAuto(\n view: Window,\n rect: DOMRect,\n panelWidth: number,\n panelHeight: number,\n gap: number\n): AnchoredCorner {\n const fitsBelow = rect.bottom + panelHeight + gap <= view.innerHeight;\n const fitsRight = rect.left + panelWidth <= view.innerWidth;\n if (fitsBelow) return fitsRight ? 'bottom-left' : 'bottom-right';\n return fitsRight ? 'top-left' : 'top-right';\n}\n\nconst samePosition = (a: AnchoredPosition, b: AnchoredPosition): boolean =>\n a.position === b.position &&\n a.top === b.top &&\n a.left === b.left &&\n a.minWidth === b.minWidth;\n\n/**\n * Places a floating panel against its anchor: resolves `auto` to a corner\n * and, for a panel fixed to the viewport, works out its coordinates. It\n * measures again on resize and on any scroll of the anchor's window (an\n * iframe's, when the anchor renders into one), so a fixed panel follows its\n * anchor, and with `followContent` after each render too. A measurement that\n * changes nothing doesn't render again.\n *\n * Measuring happens in a layout effect, so the panel is placed before it\n * paints. It needs both elements on the page while `active` is on, and\n * keeps the last placement when either is missing.\n *\n * @function useAnchoredPosition\n * @param anchorRef - The element the panel opens from.\n * @param panelRef - The floating panel.\n * @param options - Whether to measure, the corner asked for, and how the panel is placed.\n * @returns The corner the panel opens from, with coordinates when it is fixed.\n */\nexport function useAnchoredPosition(\n anchorRef: RefObject<HTMLElement | null>,\n panelRef: RefObject<HTMLElement | null>,\n {\n active,\n position,\n fixed,\n offset = 4,\n styledGap,\n matchWidth = false,\n followContent = false,\n }: UseAnchoredPositionOptions\n): AnchoredPosition {\n const [resolved, setResolved] = useState<AnchoredPosition>({\n position: position === 'auto' ? 'bottom-left' : position,\n });\n // The measurement while the panel is active, for `followContent` to run\n // again after a render.\n const updateRef = useRef<(() => void) | null>(null);\n\n useClientLayoutEffect(() => {\n if (!active) return undefined;\n const view =\n (anchorRef.current ?? panelRef.current)?.ownerDocument.defaultView ??\n window;\n const update = () => {\n const anchor = anchorRef.current;\n const panel = panelRef.current;\n if (!anchor || !panel) return;\n const rect = anchor.getBoundingClientRect();\n const measured = panel.getBoundingClientRect();\n // The width the panel has once it's at least as wide as the anchor.\n const width = matchWidth\n ? Math.max(measured.width, rect.width)\n : measured.width;\n const { height } = measured;\n const corner =\n position === 'auto'\n ? resolveAuto(\n view,\n rect,\n width,\n height,\n offset + (styledGap?.(panel) ?? 0)\n )\n : position;\n const next: AnchoredPosition = fixed\n ? {\n position: corner,\n top: corner.startsWith('bottom')\n ? rect.bottom + offset\n : rect.top - height - offset,\n left: corner.endsWith('left') ? rect.left : rect.right - width,\n ...(matchWidth ? { minWidth: rect.width } : {}),\n }\n : { position: corner };\n setResolved(prev => (samePosition(prev, next) ? prev : next));\n };\n updateRef.current = update;\n update();\n view.addEventListener('resize', update, { passive: true });\n view.addEventListener('scroll', update, { passive: true, capture: true });\n return () => {\n updateRef.current = null;\n view.removeEventListener('resize', update);\n view.removeEventListener('scroll', update, { capture: true });\n };\n }, [\n active,\n anchorRef,\n panelRef,\n position,\n fixed,\n offset,\n styledGap,\n matchWidth,\n ]);\n\n // After every render, on purpose: the panel's content is what changed.\n useClientLayoutEffect(() => {\n if (followContent) updateRef.current?.();\n });\n\n return resolved;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useAnchoredPosition.js","sources":["../../../src/helpers/useAnchoredPosition.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useRef, useState } from 'react';\nimport type { RefObject } from 'react';\nimport type { PickerPosition } from '../form/_pickerInternals/pickerTypes';\nimport { useClientLayoutEffect } from './useClientLayoutEffect';\n\n/** A corner of the anchor the panel opens from, once `auto` is resolved. */\nexport type AnchoredCorner = Exclude<PickerPosition, 'auto'>;\n\n/**\n * Where the panel goes: the corner it opens from and, when it is fixed to\n * the viewport, its coordinates there.\n */\nexport interface AnchoredPosition {\n position: AnchoredCorner;\n top?: number;\n left?: number;\n /**\n * With `matchWidth`, the anchor's width, which a fixed panel takes as its\n * `min-width`.\n */\n minWidth?: number;\n}\n\n/**\n * Options for `useAnchoredPosition`.\n */\nexport interface UseAnchoredPositionOptions {\n /** Whether the panel is on the page. Nothing is measured while it is not. */\n active: boolean;\n /** The corner asked for, or `auto` to pick one that keeps the panel in the viewport. */\n position: PickerPosition;\n /**\n * Whether the panel is fixed to the viewport (it was portaled away from the\n * anchor), which needs coordinates. Otherwise the panel's CSS places it\n * against the anchor from the corner alone.\n */\n fixed: boolean;\n /**\n * The gap between the anchor and the panel, in pixels, that the hook puts\n * there itself: added to the fixed coordinates and to the room `auto` asks\n * for below the anchor. Popover and the pickers' popover pass `0` and take\n * their gap from a margin in their stylesheet, read through `styledGap`, so\n * a theme's offset moves the panel and `auto` alike. A new panel should do\n * the same rather than rely on this default, which no stylesheet sees.\n * @defaultValue 4\n */\n offset?: number;\n /**\n * Reads the gap the panel's own stylesheet puts between it and the anchor,\n * in pixels, each time the panel is measured. `auto` leaves room for it as\n * well; the coordinates don't include it, because the stylesheet applies it.\n */\n styledGap?: (panel: HTMLElement) => number;\n /**\n * Whether the panel is at least as wide as the anchor, as a list under its\n * field is. A panel in place gets that from its stylesheet. A fixed one\n * can't, so its placement carries the anchor's width as `minWidth` for the\n * panel to set, and its coordinates and `auto` already allow for it.\n * @defaultValue false\n */\n matchWidth?: boolean;\n /**\n * Measures again after each render of the component holding the hook, for\n * a panel whose content changes while it's open, as a filtered list's\n * does. A fixed panel above its anchor is placed from its height, and\n * `auto` reads the height too, so a measurement taken when it opened goes\n * stale as the content changes.\n * @defaultValue false\n */\n followContent?: boolean;\n}\n\n/**\n * A `styledGap` for a panel whose stylesheet puts the gap in a margin on the\n * side facing the anchor, negative on a portaled panel above it, so the\n * larger magnitude is the gap. In pixels.\n */\nexport function marginGap(panel: HTMLElement): number {\n const { marginTop, marginBottom } = getComputedStyle(panel);\n return Math.max(\n Math.abs(parseFloat(marginTop)) || 0,\n Math.abs(parseFloat(marginBottom)) || 0\n );\n}\n\n/**\n * The corner that keeps a panel of this size in the viewport, preferring\n * below the anchor and lined up with its left edge.\n */\nfunction resolveAuto(\n view: Window,\n rect: DOMRect,\n panelWidth: number,\n panelHeight: number,\n gap: number\n): AnchoredCorner {\n const fitsBelow = rect.bottom + panelHeight + gap <= view.innerHeight;\n const fitsRight = rect.left + panelWidth <= view.innerWidth;\n if (fitsBelow) return fitsRight ? 'bottom-left' : 'bottom-right';\n return fitsRight ? 'top-left' : 'top-right';\n}\n\nconst samePosition = (a: AnchoredPosition, b: AnchoredPosition): boolean =>\n a.position === b.position &&\n a.top === b.top &&\n a.left === b.left &&\n a.minWidth === b.minWidth;\n\n/**\n * Places a floating panel against its anchor: resolves `auto` to a corner\n * and, for a panel fixed to the viewport, works out its coordinates. It\n * measures again on resize and on any scroll of the anchor's window (an\n * iframe's, when the anchor renders into one), so a fixed panel follows its\n * anchor, and with `followContent` after each render too. A measurement that\n * changes nothing doesn't render again.\n *\n * Measuring happens in a layout effect, so the panel is placed before it\n * paints. It needs both elements on the page while `active` is on, and\n * keeps the last placement when either is missing.\n *\n * @function useAnchoredPosition\n * @param anchorRef - The element the panel opens from.\n * @param panelRef - The floating panel.\n * @param options - Whether to measure, the corner asked for, and how the panel is placed.\n * @returns The corner the panel opens from, with coordinates when it is fixed.\n */\nexport function useAnchoredPosition(\n anchorRef: RefObject<HTMLElement | null>,\n panelRef: RefObject<HTMLElement | null>,\n {\n active,\n position,\n fixed,\n offset = 4,\n styledGap,\n matchWidth = false,\n followContent = false,\n }: UseAnchoredPositionOptions\n): AnchoredPosition {\n const [resolved, setResolved] = useState<AnchoredPosition>({\n position: position === 'auto' ? 'bottom-left' : position,\n });\n // The measurement while the panel is active, for `followContent` to run\n // again after a render.\n const updateRef = useRef<(() => void) | null>(null);\n\n useClientLayoutEffect(() => {\n if (!active) return undefined;\n const view =\n (anchorRef.current ?? panelRef.current)?.ownerDocument.defaultView ??\n window;\n const update = () => {\n const anchor = anchorRef.current;\n const panel = panelRef.current;\n if (!anchor || !panel) return;\n const rect = anchor.getBoundingClientRect();\n const measured = panel.getBoundingClientRect();\n // The width the panel has once it's at least as wide as the anchor.\n const width = matchWidth\n ? Math.max(measured.width, rect.width)\n : measured.width;\n const { height } = measured;\n const corner =\n position === 'auto'\n ? resolveAuto(\n view,\n rect,\n width,\n height,\n offset + (styledGap?.(panel) ?? 0)\n )\n : position;\n const next: AnchoredPosition = fixed\n ? {\n position: corner,\n top: corner.startsWith('bottom')\n ? rect.bottom + offset\n : rect.top - height - offset,\n left: corner.endsWith('left') ? rect.left : rect.right - width,\n ...(matchWidth ? { minWidth: rect.width } : {}),\n }\n : { position: corner };\n setResolved(prev => (samePosition(prev, next) ? prev : next));\n };\n updateRef.current = update;\n update();\n view.addEventListener('resize', update, { passive: true });\n view.addEventListener('scroll', update, { passive: true, capture: true });\n return () => {\n updateRef.current = null;\n view.removeEventListener('resize', update);\n view.removeEventListener('scroll', update, { capture: true });\n };\n }, [\n active,\n anchorRef,\n panelRef,\n position,\n fixed,\n offset,\n styledGap,\n matchWidth,\n ]);\n\n // After every render, on purpose: the panel's content is what changed.\n useClientLayoutEffect(() => {\n if (followContent) updateRef.current?.();\n });\n\n return resolved;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
'use client';
|
|
2
3
|
import { useMemo } from 'react';
|
|
3
4
|
import { classNames } from './classNames.js';
|
|
4
5
|
import { useColorClasses, useColorStyles } from './useColorClasses.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBulmaClasses.js","sources":["../../../src/helpers/useBulmaClasses.tsx"],"sourcesContent":["import { useMemo, type CSSProperties } from 'react';\nimport { classNames } from '../helpers/classNames';\nimport {\n BulmaDisplayProps,\n BulmaViewportProps,\n type BulmaGapStep,\n} from './bulmaClassHelpers';\nimport {\n useColorClasses,\n useColorStyles,\n BulmaColorProps,\n BulmaColorPropsWithScheme,\n} from './useColorClasses';\nimport { useSpacingClasses, BulmaSpacingProps } from './useSpacingClasses';\nimport {\n useTypographyClasses,\n BulmaTypographyProps,\n} from './useTypographyClasses';\nimport {\n useVisibilityClasses,\n BulmaVisibilityProps,\n} from './useVisibilityClasses';\nimport { useFlexboxClasses, BulmaFlexboxProps } from './useFlexboxClasses';\nimport { useOtherClasses, BulmaOtherProps } from './useOtherClasses';\n\n/**\n * Props for applying Bulma helper classes to components.\n */\nexport interface BulmaClassesProps\n extends\n BulmaColorProps,\n BulmaSpacingProps,\n BulmaTypographyProps,\n BulmaVisibilityProps,\n BulmaFlexboxProps,\n BulmaOtherProps {}\n\n/**\n * {@link BulmaClassesProps} with `backgroundColor` widened to also accept the\n * scheme color names from `validSchemeColors` (see\n * {@link BulmaColorPropsWithScheme}). This is the input type of\n * `useBulmaClasses`; scheme values surface as the `bulmaHelperStyles` return\n * member instead of a class.\n */\nexport type BulmaClassesPropsWithScheme = Omit<\n BulmaClassesProps,\n keyof BulmaColorProps\n> &\n BulmaColorPropsWithScheme;\n\n/**\n * A hook that generates Bulma helper classes from props and separates unhandled props.\n *\n * Composed from the per-concern mini hooks (useColorClasses, useSpacingClasses,\n * useTypographyClasses, useVisibilityClasses, useFlexboxClasses, and\n * useOtherClasses), which can also be used individually.\n *\n * Scheme background values (`backgroundColor: 'scheme-main-bis'`, …) emit no\n * class; they are returned as `bulmaHelperStyles` — a dark-mode-safe inline\n * `background-color: var(--bulma-scheme-*)` style (`undefined` for all other\n * inputs). Merge it with a user `style` prop via `mergeBulmaStyles`.\n *\n * @function useBulmaClasses\n * @param props - Combination of BulmaClassesPropsWithScheme and additional props.\n * @returns An object containing the Bulma helper classes, optional helper\n * styles, and unhandled props.\n * @example\n * const { bulmaHelperClasses, bulmaHelperStyles, rest } = useBulmaClasses({\n * color: 'primary',\n * backgroundColor: 'scheme-main-bis',\n * className: 'custom-class'\n * });\n * // bulmaHelperClasses: 'has-text-primary'\n * // bulmaHelperStyles: { backgroundColor: 'var(--bulma-scheme-main-bis)' }\n * // rest: { className: 'custom-class' }\n */\nexport const useBulmaClasses = <T extends object>(\n props: BulmaClassesPropsWithScheme & T\n): {\n bulmaHelperClasses: string;\n bulmaHelperStyles?: CSSProperties;\n rest: Omit<T, keyof BulmaClassesProps>;\n} => {\n const {\n color,\n backgroundColor,\n colorShade,\n backgroundColorShade,\n m,\n mt,\n mr,\n mb,\n ml,\n mx,\n my,\n p,\n pt,\n pr,\n pb,\n pl,\n px,\n py,\n gap,\n columnGap,\n rowGap,\n gapless,\n textSize,\n textAlign,\n textTransform,\n textWeight,\n fontFamily,\n display,\n visibility,\n flexDirection,\n flexWrap,\n justifyContent,\n alignContent,\n alignItems,\n alignSelf,\n flexGrow,\n flexShrink,\n float,\n overflow,\n overflowX,\n overflowY,\n overlay,\n interaction,\n cursor,\n radius,\n shadow,\n responsive,\n viewport,\n displayMobile,\n displayTablet,\n displayTabletOnly,\n displayTouch,\n displayDesktop,\n displayDesktopOnly,\n displayWidescreen,\n displayWidescreenOnly,\n displayFullhd,\n textSizeMobile,\n textSizeTablet,\n textSizeDesktop,\n textSizeWidescreen,\n textSizeFullhd,\n textAlignMobile,\n textAlignTablet,\n textAlignDesktop,\n textAlignWidescreen,\n textAlignFullhd,\n visibilityMobile,\n visibilityTablet,\n visibilityTabletOnly,\n visibilityTouch,\n visibilityDesktop,\n visibilityDesktopOnly,\n visibilityWidescreen,\n visibilityWidescreenOnly,\n visibilityFullhd,\n skeleton,\n clearfix,\n pos,\n relative,\n fullHeight,\n aspectRatio,\n ...rest\n } = props;\n\n const colorClasses = useColorClasses({\n color,\n colorShade,\n backgroundColor,\n backgroundColorShade,\n });\n\n const bulmaHelperStyles = useColorStyles({ backgroundColor });\n\n const spacingClasses = useSpacingClasses({\n m,\n mt,\n mr,\n mb,\n ml,\n mx,\n my,\n p,\n pt,\n pr,\n pb,\n pl,\n px,\n py,\n gap,\n columnGap,\n rowGap,\n gapless,\n });\n\n const typographyClasses = useTypographyClasses({\n textSize,\n textAlign,\n textTransform,\n textWeight,\n fontFamily,\n viewport,\n textSizeMobile,\n textSizeTablet,\n textSizeDesktop,\n textSizeWidescreen,\n textSizeFullhd,\n textAlignMobile,\n textAlignTablet,\n textAlignDesktop,\n textAlignWidescreen,\n textAlignFullhd,\n });\n\n const visibilityClasses = useVisibilityClasses({\n visibility,\n visibilityMobile,\n visibilityTablet,\n visibilityTabletOnly,\n visibilityTouch,\n visibilityDesktop,\n visibilityDesktopOnly,\n visibilityWidescreen,\n visibilityWidescreenOnly,\n visibilityFullhd,\n display,\n displayMobile,\n displayTablet,\n displayTabletOnly,\n displayTouch,\n displayDesktop,\n displayDesktopOnly,\n displayWidescreen,\n displayWidescreenOnly,\n displayFullhd,\n viewport,\n });\n\n const flexboxClasses = useFlexboxClasses({\n flexDirection,\n flexWrap,\n justifyContent,\n alignContent,\n alignItems,\n alignSelf,\n flexGrow,\n flexShrink,\n display,\n displayMobile,\n displayTablet,\n displayTabletOnly,\n displayTouch,\n displayDesktop,\n displayDesktopOnly,\n displayWidescreen,\n displayWidescreenOnly,\n displayFullhd,\n });\n\n const otherClasses = useOtherClasses({\n float,\n overflow,\n overflowX,\n overflowY,\n overlay,\n interaction,\n cursor,\n radius,\n shadow,\n responsive,\n skeleton,\n clearfix,\n pos,\n relative,\n fullHeight,\n aspectRatio,\n });\n\n const bulmaHelperClasses = useMemo(\n () =>\n classNames(\n colorClasses,\n spacingClasses,\n typographyClasses,\n visibilityClasses,\n flexboxClasses,\n otherClasses\n ),\n [\n colorClasses,\n spacingClasses,\n typographyClasses,\n visibilityClasses,\n flexboxClasses,\n otherClasses,\n ]\n );\n\n return { bulmaHelperClasses, bulmaHelperStyles, rest };\n};\n\nexport {\n validColors,\n validColorShades,\n validSchemeColors,\n validSizes,\n validGaps,\n validTextSizes,\n validAlignments,\n validTextTransforms,\n validTextWeights,\n validFontFamilies,\n validDisplays,\n validVisibilities,\n validFlexDirections,\n validFlexWraps,\n validJustifyContents,\n validAlignContents,\n validAlignItems,\n validAlignSelfs,\n validFlexGrowShrink,\n validViewports,\n validFloats,\n validOverflows,\n validAxisOverflows,\n validInteractions,\n validCursors,\n validRadii,\n validShadows,\n validResponsives,\n validPositions,\n validAspectRatios,\n} from './bulmaClassHelpers';\nexport type { BulmaViewportProps, BulmaDisplayProps, BulmaGapStep };\nexport * from './useColorClasses';\nexport * from './useSpacingClasses';\nexport * from './useTypographyClasses';\nexport * from './useVisibilityClasses';\nexport * from './useFlexboxClasses';\nexport * from './useOtherClasses';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useBulmaClasses.js","sources":["../../../src/helpers/useBulmaClasses.tsx"],"sourcesContent":["import { useMemo, type CSSProperties } from 'react';\nimport { classNames } from '../helpers/classNames';\nimport {\n BulmaDisplayProps,\n BulmaViewportProps,\n type BulmaGapStep,\n} from './bulmaClassHelpers';\nimport {\n useColorClasses,\n useColorStyles,\n BulmaColorProps,\n BulmaColorPropsWithScheme,\n} from './useColorClasses';\nimport { useSpacingClasses, BulmaSpacingProps } from './useSpacingClasses';\nimport {\n useTypographyClasses,\n BulmaTypographyProps,\n} from './useTypographyClasses';\nimport {\n useVisibilityClasses,\n BulmaVisibilityProps,\n} from './useVisibilityClasses';\nimport { useFlexboxClasses, BulmaFlexboxProps } from './useFlexboxClasses';\nimport { useOtherClasses, BulmaOtherProps } from './useOtherClasses';\n\n/**\n * Props for applying Bulma helper classes to components.\n */\nexport interface BulmaClassesProps\n extends\n BulmaColorProps,\n BulmaSpacingProps,\n BulmaTypographyProps,\n BulmaVisibilityProps,\n BulmaFlexboxProps,\n BulmaOtherProps {}\n\n/**\n * {@link BulmaClassesProps} with `backgroundColor` widened to also accept the\n * scheme color names from `validSchemeColors` (see\n * {@link BulmaColorPropsWithScheme}). This is the input type of\n * `useBulmaClasses`; scheme values surface as the `bulmaHelperStyles` return\n * member instead of a class.\n */\nexport type BulmaClassesPropsWithScheme = Omit<\n BulmaClassesProps,\n keyof BulmaColorProps\n> &\n BulmaColorPropsWithScheme;\n\n/**\n * A hook that generates Bulma helper classes from props and separates unhandled props.\n *\n * Composed from the per-concern mini hooks (useColorClasses, useSpacingClasses,\n * useTypographyClasses, useVisibilityClasses, useFlexboxClasses, and\n * useOtherClasses), which can also be used individually.\n *\n * Scheme background values (`backgroundColor: 'scheme-main-bis'`, …) emit no\n * class; they are returned as `bulmaHelperStyles` — a dark-mode-safe inline\n * `background-color: var(--bulma-scheme-*)` style (`undefined` for all other\n * inputs). Merge it with a user `style` prop via `mergeBulmaStyles`.\n *\n * @function useBulmaClasses\n * @param props - Combination of BulmaClassesPropsWithScheme and additional props.\n * @returns An object containing the Bulma helper classes, optional helper\n * styles, and unhandled props.\n * @example\n * const { bulmaHelperClasses, bulmaHelperStyles, rest } = useBulmaClasses({\n * color: 'primary',\n * backgroundColor: 'scheme-main-bis',\n * className: 'custom-class'\n * });\n * // bulmaHelperClasses: 'has-text-primary'\n * // bulmaHelperStyles: { backgroundColor: 'var(--bulma-scheme-main-bis)' }\n * // rest: { className: 'custom-class' }\n */\nexport const useBulmaClasses = <T extends object>(\n props: BulmaClassesPropsWithScheme & T\n): {\n bulmaHelperClasses: string;\n bulmaHelperStyles?: CSSProperties;\n rest: Omit<T, keyof BulmaClassesProps>;\n} => {\n const {\n color,\n backgroundColor,\n colorShade,\n backgroundColorShade,\n m,\n mt,\n mr,\n mb,\n ml,\n mx,\n my,\n p,\n pt,\n pr,\n pb,\n pl,\n px,\n py,\n gap,\n columnGap,\n rowGap,\n gapless,\n textSize,\n textAlign,\n textTransform,\n textWeight,\n fontFamily,\n display,\n visibility,\n flexDirection,\n flexWrap,\n justifyContent,\n alignContent,\n alignItems,\n alignSelf,\n flexGrow,\n flexShrink,\n float,\n overflow,\n overflowX,\n overflowY,\n overlay,\n interaction,\n cursor,\n radius,\n shadow,\n responsive,\n viewport,\n displayMobile,\n displayTablet,\n displayTabletOnly,\n displayTouch,\n displayDesktop,\n displayDesktopOnly,\n displayWidescreen,\n displayWidescreenOnly,\n displayFullhd,\n textSizeMobile,\n textSizeTablet,\n textSizeDesktop,\n textSizeWidescreen,\n textSizeFullhd,\n textAlignMobile,\n textAlignTablet,\n textAlignDesktop,\n textAlignWidescreen,\n textAlignFullhd,\n visibilityMobile,\n visibilityTablet,\n visibilityTabletOnly,\n visibilityTouch,\n visibilityDesktop,\n visibilityDesktopOnly,\n visibilityWidescreen,\n visibilityWidescreenOnly,\n visibilityFullhd,\n skeleton,\n clearfix,\n pos,\n relative,\n fullHeight,\n aspectRatio,\n ...rest\n } = props;\n\n const colorClasses = useColorClasses({\n color,\n colorShade,\n backgroundColor,\n backgroundColorShade,\n });\n\n const bulmaHelperStyles = useColorStyles({ backgroundColor });\n\n const spacingClasses = useSpacingClasses({\n m,\n mt,\n mr,\n mb,\n ml,\n mx,\n my,\n p,\n pt,\n pr,\n pb,\n pl,\n px,\n py,\n gap,\n columnGap,\n rowGap,\n gapless,\n });\n\n const typographyClasses = useTypographyClasses({\n textSize,\n textAlign,\n textTransform,\n textWeight,\n fontFamily,\n viewport,\n textSizeMobile,\n textSizeTablet,\n textSizeDesktop,\n textSizeWidescreen,\n textSizeFullhd,\n textAlignMobile,\n textAlignTablet,\n textAlignDesktop,\n textAlignWidescreen,\n textAlignFullhd,\n });\n\n const visibilityClasses = useVisibilityClasses({\n visibility,\n visibilityMobile,\n visibilityTablet,\n visibilityTabletOnly,\n visibilityTouch,\n visibilityDesktop,\n visibilityDesktopOnly,\n visibilityWidescreen,\n visibilityWidescreenOnly,\n visibilityFullhd,\n display,\n displayMobile,\n displayTablet,\n displayTabletOnly,\n displayTouch,\n displayDesktop,\n displayDesktopOnly,\n displayWidescreen,\n displayWidescreenOnly,\n displayFullhd,\n viewport,\n });\n\n const flexboxClasses = useFlexboxClasses({\n flexDirection,\n flexWrap,\n justifyContent,\n alignContent,\n alignItems,\n alignSelf,\n flexGrow,\n flexShrink,\n display,\n displayMobile,\n displayTablet,\n displayTabletOnly,\n displayTouch,\n displayDesktop,\n displayDesktopOnly,\n displayWidescreen,\n displayWidescreenOnly,\n displayFullhd,\n });\n\n const otherClasses = useOtherClasses({\n float,\n overflow,\n overflowX,\n overflowY,\n overlay,\n interaction,\n cursor,\n radius,\n shadow,\n responsive,\n skeleton,\n clearfix,\n pos,\n relative,\n fullHeight,\n aspectRatio,\n });\n\n const bulmaHelperClasses = useMemo(\n () =>\n classNames(\n colorClasses,\n spacingClasses,\n typographyClasses,\n visibilityClasses,\n flexboxClasses,\n otherClasses\n ),\n [\n colorClasses,\n spacingClasses,\n typographyClasses,\n visibilityClasses,\n flexboxClasses,\n otherClasses,\n ]\n );\n\n return { bulmaHelperClasses, bulmaHelperStyles, rest };\n};\n\nexport {\n validColors,\n validColorShades,\n validSchemeColors,\n validSizes,\n validGaps,\n validTextSizes,\n validAlignments,\n validTextTransforms,\n validTextWeights,\n validFontFamilies,\n validDisplays,\n validVisibilities,\n validFlexDirections,\n validFlexWraps,\n validJustifyContents,\n validAlignContents,\n validAlignItems,\n validAlignSelfs,\n validFlexGrowShrink,\n validViewports,\n validFloats,\n validOverflows,\n validAxisOverflows,\n validInteractions,\n validCursors,\n validRadii,\n validShadows,\n validResponsives,\n validPositions,\n validAspectRatios,\n} from './bulmaClassHelpers';\nexport type { BulmaViewportProps, BulmaDisplayProps, BulmaGapStep };\nexport * from './useColorClasses';\nexport * from './useSpacingClasses';\nexport * from './useTypographyClasses';\nexport * from './useVisibilityClasses';\nexport * from './useFlexboxClasses';\nexport * from './useOtherClasses';\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useClientLayoutEffect.js","sources":["../../../src/helpers/useClientLayoutEffect.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useEffect, useLayoutEffect } from 'react';\n\n/**\n * `useLayoutEffect` in the browser, `useEffect` on the server. A server\n * render runs neither, and React 18 warns about a layout effect there, so the\n * server is handed the plain one.\n */\nexport const useClientLayoutEffect =\n typeof document === 'undefined' ? useEffect : useLayoutEffect;\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useClientLayoutEffect.js","sources":["../../../src/helpers/useClientLayoutEffect.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useEffect, useLayoutEffect } from 'react';\n\n/**\n * `useLayoutEffect` in the browser, `useEffect` on the server. A server\n * render runs neither, and React 18 warns about a layout effect there, so the\n * server is handed the plain one.\n */\nexport const useClientLayoutEffect =\n typeof document === 'undefined' ? useEffect : useLayoutEffect;\n"],"names":[],"mappings":";;;;AAAA;AAGA;;;;AAIG;AACI,MAAM,qBAAqB,GAChC,OAAO,QAAQ,KAAK,WAAW,GAAG,SAAS,GAAG;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useColorClasses.js","sources":["../../../src/helpers/useColorClasses.tsx"],"sourcesContent":["import { useMemo, type CSSProperties } from 'react';\nimport { classNames } from './classNames';\nimport { useConfig } from './Config';\nimport {\n createBulmaClassHelpers,\n validColors,\n validColorShades,\n validSchemeColors,\n} from './bulmaClassHelpers';\n\n/**\n * Props for applying Bulma text and background color helper classes.\n */\nexport interface BulmaColorProps {\n /** Text color class (e.g., 'primary', 'info'). */\n color?: (typeof validColors)[number] | 'inherit' | 'current';\n /** Text color shade suffix (e.g., '00', 'invert'). */\n colorShade?: (typeof validColorShades)[number];\n /** Background color class (e.g., 'primary', 'info'). */\n backgroundColor?: (typeof validColors)[number] | 'inherit' | 'current';\n /** Background color shade suffix (e.g., '00', 'invert'). */\n backgroundColorShade?: (typeof validColorShades)[number];\n}\n\n/**\n * {@link BulmaColorProps} with `backgroundColor` widened to also accept the\n * scheme color names from `validSchemeColors` (e.g., 'scheme-main-bis').\n *\n * Scheme values never emit a `has-background-*` class — pass them through\n * {@link useColorStyles} (or `useBulmaClasses`' `bulmaHelperStyles` return) to\n * get the dark-mode-safe inline `background-color: var(--bulma-scheme-*)`\n * style instead.\n */\nexport type BulmaColorPropsWithScheme = Omit<\n BulmaColorProps,\n 'backgroundColor'\n> & {\n /** Background color: a Bulma color class name or a scheme color name. */\n backgroundColor?:\n BulmaColorProps['backgroundColor'] | (typeof validSchemeColors)[number];\n};\n\n/**\n * A hook that generates Bulma text and background color helper classes.\n *\n * Scheme background values (`scheme-main`, `scheme-main-bis`, …) are dropped\n * from class emission — Bulma ships no `has-background-scheme-*` classes; use\n * {@link useColorStyles} to render them as an inline style.\n *\n * @function useColorClasses\n * @param props - Color-related Bulma helper props.\n * @returns A space-separated string of color helper classes.\n * @example\n * const colorClasses = useColorClasses({\n * color: 'primary',\n * backgroundColor: 'info',\n * });\n * // colorClasses: 'has-text-primary has-background-info'\n */\nexport const useColorClasses = (props: BulmaColorPropsWithScheme): string => {\n const { classPrefix } = useConfig();\n\n const { color, colorShade, backgroundColor, backgroundColorShade } = props;\n\n return useMemo(() => {\n const { classes, addPrefixedClass, addClass } =\n createBulmaClassHelpers(classPrefix);\n\n // Color handling\n const addColorClass = (\n prefix: 'has-text' | 'has-background',\n value: string | undefined,\n shade: (typeof validColorShades)[number] | undefined\n ) => {\n if (!value || ![...validColors, 'inherit', 'current'].includes(value))\n return;\n if (shade && validColorShades.includes(shade)) {\n // Color shades never support viewport modifiers in Bulma\n const className = `${prefix}-${value}-${shade}`;\n addPrefixedClass(className);\n } else {\n // Color classes never support viewport modifiers in Bulma\n addClass(\n prefix,\n value,\n [...validColors, 'inherit', 'current'],\n false // supportsViewport = false for all color classes\n );\n }\n };\n\n // Color\n addColorClass('has-text', color, colorShade);\n addColorClass('has-background', backgroundColor, backgroundColorShade);\n\n return classNames(classes);\n }, [classPrefix, color, colorShade, backgroundColor, backgroundColorShade]);\n};\n\n/**\n * A hook that generates inline styles for scheme-aware color props.\n *\n * When `backgroundColor` is one of the scheme color names in\n * `validSchemeColors`, returns `{ backgroundColor: 'var(--bulma-<value>)' }` —\n * an inline style that tracks Bulma's scheme CSS variables, so it adapts to\n * light/dark mode with no bestax-shipped CSS. For every other input it\n * returns `undefined` (never `{}`), so components that don't use scheme\n * values produce identical DOM to before.\n *\n * `backgroundColorShade` is ignored for scheme values — Bulma defines no\n * shaded scheme variables. Only the background is set: the `scheme-invert*`\n * values do not change text color, so pair them with a contrasting\n * foreground.\n *\n * @function useColorStyles\n * @param props - Color-related Bulma helper props (scheme-aware).\n * @returns An inline style object for scheme backgrounds, or `undefined`.\n * @example\n * const colorStyles = useColorStyles({ backgroundColor: 'scheme-main-bis' });\n * // colorStyles: { backgroundColor: 'var(--bulma-scheme-main-bis)' }\n */\nexport const useColorStyles = (\n props: BulmaColorPropsWithScheme\n): CSSProperties | undefined => {\n const { backgroundColor } = props;\n\n return useMemo(() => {\n if (\n backgroundColor &&\n (validSchemeColors as readonly string[]).includes(backgroundColor)\n ) {\n return { backgroundColor: `var(--bulma-${backgroundColor})` };\n }\n return undefined;\n }, [backgroundColor]);\n};\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useColorClasses.js","sources":["../../../src/helpers/useColorClasses.tsx"],"sourcesContent":["import { useMemo, type CSSProperties } from 'react';\nimport { classNames } from './classNames';\nimport { useConfig } from './Config';\nimport {\n createBulmaClassHelpers,\n validColors,\n validColorShades,\n validSchemeColors,\n} from './bulmaClassHelpers';\n\n/**\n * Props for applying Bulma text and background color helper classes.\n */\nexport interface BulmaColorProps {\n /** Text color class (e.g., 'primary', 'info'). */\n color?: (typeof validColors)[number] | 'inherit' | 'current';\n /** Text color shade suffix (e.g., '00', 'invert'). */\n colorShade?: (typeof validColorShades)[number];\n /** Background color class (e.g., 'primary', 'info'). */\n backgroundColor?: (typeof validColors)[number] | 'inherit' | 'current';\n /** Background color shade suffix (e.g., '00', 'invert'). */\n backgroundColorShade?: (typeof validColorShades)[number];\n}\n\n/**\n * {@link BulmaColorProps} with `backgroundColor` widened to also accept the\n * scheme color names from `validSchemeColors` (e.g., 'scheme-main-bis').\n *\n * Scheme values never emit a `has-background-*` class — pass them through\n * {@link useColorStyles} (or `useBulmaClasses`' `bulmaHelperStyles` return) to\n * get the dark-mode-safe inline `background-color: var(--bulma-scheme-*)`\n * style instead.\n */\nexport type BulmaColorPropsWithScheme = Omit<\n BulmaColorProps,\n 'backgroundColor'\n> & {\n /** Background color: a Bulma color class name or a scheme color name. */\n backgroundColor?:\n BulmaColorProps['backgroundColor'] | (typeof validSchemeColors)[number];\n};\n\n/**\n * A hook that generates Bulma text and background color helper classes.\n *\n * Scheme background values (`scheme-main`, `scheme-main-bis`, …) are dropped\n * from class emission — Bulma ships no `has-background-scheme-*` classes; use\n * {@link useColorStyles} to render them as an inline style.\n *\n * @function useColorClasses\n * @param props - Color-related Bulma helper props.\n * @returns A space-separated string of color helper classes.\n * @example\n * const colorClasses = useColorClasses({\n * color: 'primary',\n * backgroundColor: 'info',\n * });\n * // colorClasses: 'has-text-primary has-background-info'\n */\nexport const useColorClasses = (props: BulmaColorPropsWithScheme): string => {\n const { classPrefix } = useConfig();\n\n const { color, colorShade, backgroundColor, backgroundColorShade } = props;\n\n return useMemo(() => {\n const { classes, addPrefixedClass, addClass } =\n createBulmaClassHelpers(classPrefix);\n\n // Color handling\n const addColorClass = (\n prefix: 'has-text' | 'has-background',\n value: string | undefined,\n shade: (typeof validColorShades)[number] | undefined\n ) => {\n if (!value || ![...validColors, 'inherit', 'current'].includes(value))\n return;\n if (shade && validColorShades.includes(shade)) {\n // Color shades never support viewport modifiers in Bulma\n const className = `${prefix}-${value}-${shade}`;\n addPrefixedClass(className);\n } else {\n // Color classes never support viewport modifiers in Bulma\n addClass(\n prefix,\n value,\n [...validColors, 'inherit', 'current'],\n false // supportsViewport = false for all color classes\n );\n }\n };\n\n // Color\n addColorClass('has-text', color, colorShade);\n addColorClass('has-background', backgroundColor, backgroundColorShade);\n\n return classNames(classes);\n }, [classPrefix, color, colorShade, backgroundColor, backgroundColorShade]);\n};\n\n/**\n * A hook that generates inline styles for scheme-aware color props.\n *\n * When `backgroundColor` is one of the scheme color names in\n * `validSchemeColors`, returns `{ backgroundColor: 'var(--bulma-<value>)' }` —\n * an inline style that tracks Bulma's scheme CSS variables, so it adapts to\n * light/dark mode with no bestax-shipped CSS. For every other input it\n * returns `undefined` (never `{}`), so components that don't use scheme\n * values produce identical DOM to before.\n *\n * `backgroundColorShade` is ignored for scheme values — Bulma defines no\n * shaded scheme variables. Only the background is set: the `scheme-invert*`\n * values do not change text color, so pair them with a contrasting\n * foreground.\n *\n * @function useColorStyles\n * @param props - Color-related Bulma helper props (scheme-aware).\n * @returns An inline style object for scheme backgrounds, or `undefined`.\n * @example\n * const colorStyles = useColorStyles({ backgroundColor: 'scheme-main-bis' });\n * // colorStyles: { backgroundColor: 'var(--bulma-scheme-main-bis)' }\n */\nexport const useColorStyles = (\n props: BulmaColorPropsWithScheme\n): CSSProperties | undefined => {\n const { backgroundColor } = props;\n\n return useMemo(() => {\n if (\n backgroundColor &&\n (validSchemeColors as readonly string[]).includes(backgroundColor)\n ) {\n return { backgroundColor: `var(--bulma-${backgroundColor})` };\n }\n return undefined;\n }, [backgroundColor]);\n};\n"],"names":[],"mappings":";;;;;;;AA0CA;;;;;;;;;;;;;;;;AAgBG;AACI,MAAM,eAAe,GAAG,CAAC,KAAgC,KAAY;AAC1E,IAAA,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,EAAE;IAEnC,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,oBAAoB,EAAE,GAAG,KAAK;IAE1E,OAAO,OAAO,CAAC,MAAK;AAClB,QAAA,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,GAC3C,uBAAuB,CAAC,WAAW,CAAC;;QAGtC,MAAM,aAAa,GAAG,CACpB,MAAqC,EACrC,KAAyB,EACzB,KAAoD,KAClD;AACF,YAAA,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;gBACnE;YACF,IAAI,KAAK,IAAI,gBAAgB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE;;gBAE7C,MAAM,SAAS,GAAG,CAAA,EAAG,MAAM,IAAI,KAAK,CAAA,CAAA,EAAI,KAAK,CAAA,CAAE;gBAC/C,gBAAgB,CAAC,SAAS,CAAC;YAC7B;iBAAO;;AAEL,gBAAA,QAAQ,CACN,MAAM,EACN,KAAK,EACL,CAAC,GAAG,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC,EACtC,KAAK;iBACN;YACH;AACF,QAAA,CAAC;;AAGD,QAAA,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,UAAU,CAAC;AAC5C,QAAA,aAAa,CAAC,gBAAgB,EAAE,eAAe,EAAE,oBAAoB,CAAC;AAEtE,QAAA,OAAO,UAAU,CAAC,OAAO,CAAC;AAC5B,IAAA,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,oBAAoB,CAAC,CAAC;AAC7E;AAEA;;;;;;;;;;;;;;;;;;;;;AAqBG;AACI,MAAM,cAAc,GAAG,CAC5B,KAAgC,KACH;AAC7B,IAAA,MAAM,EAAE,eAAe,EAAE,GAAG,KAAK;IAEjC,OAAO,OAAO,CAAC,MAAK;AAClB,QAAA,IACE,eAAe;AACd,YAAA,iBAAuC,CAAC,QAAQ,CAAC,eAAe,CAAC,EAClE;AACA,YAAA,OAAO,EAAE,eAAe,EAAE,eAAe,eAAe,CAAA,CAAA,CAAG,EAAE;QAC/D;AACA,QAAA,OAAO,SAAS;AAClB,IAAA,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC;AACvB;;"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
'use client';
|
|
2
3
|
import { useId, useCallback, useRef, useState, useReducer, useEffect } from 'react';
|
|
3
4
|
import { useClientLayoutEffect } from './useClientLayoutEffect.js';
|
|
4
5
|
import { getActiveElementInTree, isEventInside } from './shadowDom.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useComboboxEngine.js","sources":["../../../src/helpers/useComboboxEngine.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport {\n useCallback,\n useEffect,\n useId,\n useReducer,\n useRef,\n useState,\n} from 'react';\nimport type React from 'react';\nimport { useClientLayoutEffect } from './useClientLayoutEffect';\nimport { getActiveElementInTree, isEventInside } from './shadowDom';\n\n/** What an item tells the engine about itself. */\nexport interface ComboboxItemState {\n /** What a pick hands back. Unique within the combobox. */\n value: string;\n /** Reachable with the arrows, never picked. */\n disabled: boolean;\n /** The value, or one of them. */\n checked: boolean;\n}\n\n/** An option on the page, with its element. */\ninterface ShownOption extends ComboboxItemState {\n element: HTMLElement;\n}\n\n/**\n * Where the highlight lands once an opened list has rendered, or once typing\n * has changed what's in it:\n *\n * - `first` and `last` (Down and Up Arrow): the checked option, otherwise the\n * first or last enabled one;\n * - `open` (a click, or any other way of opening): the checked option,\n * otherwise the first enabled one with `keepFirst`, otherwise nothing;\n * - `typed`: the first enabled option with `keepFirst`, otherwise nothing;\n * - `none` (Alt+Down Arrow): nothing.\n *\n * Only an enabled option takes it, so a disabled checked one is passed over.\n */\ntype Landing = 'first' | 'last' | 'open' | 'typed' | 'none';\n\n/** Options for `useComboboxEngine`. */\nexport interface ComboboxEngineOptions {\n /** Controlled open state. When set, opening and closing only report through `onOpenChange`. */\n open?: boolean;\n /** Initial open state when uncontrolled. */\n defaultOpen: boolean;\n /** Called with the open state asked for, when it differs from the current one. */\n onOpenChange?: (open: boolean) => void;\n /** Called with an item's `value` when it's picked. */\n onSelect?: (value: string) => void;\n /**\n * Called when Enter goes on to the form with nothing to pick, before the\n * list closes, so an owner that shows something other than the typing can\n * put it back before the form reads the field. An `inline` list doesn't\n * close, so this is the one word it gets.\n */\n onSettle?: () => void;\n /** Highlights the first enabled item whenever the list opens or its items change. */\n keepFirst: boolean;\n /** Several values: a pick keeps the list open. */\n multiple: boolean;\n /** The list is always on screen, so nothing opens or closes it. */\n inline: boolean;\n /**\n * The list's panel is portaled, so it has no server markup and shows only\n * from the commit after hydration. Until it has rendered, an open list\n * isn't taken to be on the page, so no `aria-controls` names it early.\n */\n deferred: boolean;\n}\n\n/** What `useComboboxEngine` hands the parts of a combobox. */\nexport interface ComboboxEngine {\n /** Whether the list is open. Always, with `inline`. */\n open: boolean;\n /**\n * The field's `aria-expanded`: open, with a listbox that has an option in\n * it on the page. Until the open list has rendered, it's taken to have\n * one, so server markup for an open list in place says it's expanded. A\n * `deferred` list isn't in server markup, so it's taken to have none.\n */\n expanded: boolean;\n /** The highlighted option's value, if any. */\n highlighted: string | null;\n /** Whether the user has typed since the list opened. */\n typed: boolean;\n /** The listbox's id. */\n listboxId: string;\n /** An item's id, which follows from its value. */\n itemId: (value: string) => string;\n /** For the element around the field and, unless it's portaled, the list. */\n rootRef: React.MutableRefObject<HTMLDivElement | null>;\n /** For the field. */\n inputRef: React.MutableRefObject<HTMLInputElement | null>;\n /** For the listbox. */\n listboxRef: React.MutableRefObject<HTMLDivElement | null>;\n /**\n * Records the floating panel while it's on screen, which makes it count as\n * inside, and makes an Escape that closes it stop there. Returns the\n * function that forgets it.\n */\n registerPopup: (element: HTMLElement) => () => void;\n /** Records an item's element. Returns the function that forgets it. */\n registerItem: (element: HTMLElement, state: ComboboxItemState) => () => void;\n /** The field's keys, to run after the caller's own handler. */\n onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;\n /** A change to the field's value: the user typed. */\n onChange: () => void;\n /** A click in the field. */\n onClick: () => void;\n /** Focus leaving the field. */\n onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;\n /** The pointer moved over an enabled option. */\n hover: (value: string) => void;\n /** A click on an enabled option. Focus ends up in the field. */\n pick: (value: string) => void;\n /** For anything in the list: a press there doesn't take focus from the field. */\n keepFocus: (event: React.MouseEvent) => void;\n /**\n * For the floating panel: marks a press as inside, which React reports from\n * content the panel portals elsewhere as well.\n */\n markInside: (event: React.PointerEvent) => void;\n}\n\n/** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */\nconst isComposing = (event: React.KeyboardEvent): boolean =>\n event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;\n\nconst sameValues = (a: string[], b: string[]): boolean =>\n a.length === b.length && a.every((value, i) => value === b[i]);\n\n/**\n * The keys, the open state, the highlight and the ARIA of an editable\n * combobox whose list the app renders and filters (#1083). DOM focus stays\n * in the field the whole time: the highlight is `aria-activedescendant`, as\n * the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves\n * real focus, as a menu should.\n *\n * The options are read from the listbox in document order whenever they're\n * needed, so options in groups and options that come and go are found, and\n * the highlight is remembered by value, so it survives a re-render. The text\n * in the field and any value are the app's: a pick only calls\n * `onSelect`.\n *\n * @function useComboboxEngine\n * @param options - The open state, the pick callback and the shape.\n * @returns The state and handlers the parts wire up.\n */\nexport function useComboboxEngine({\n open: controlledOpen,\n defaultOpen,\n onOpenChange,\n onSelect,\n onSettle,\n keepFirst,\n multiple,\n inline,\n deferred,\n}: ComboboxEngineOptions): ComboboxEngine {\n const baseId = useId();\n const listboxId = `${baseId}-listbox`;\n const itemPrefix = `${baseId}-item-`;\n // The value goes into the id encoded, so a value with a space in it still\n // makes one id, not two.\n const itemId = useCallback(\n (value: string) => itemPrefix + encodeURIComponent(value),\n [itemPrefix]\n );\n\n const rootRef = useRef<HTMLDivElement | null>(null);\n const inputRef = useRef<HTMLInputElement | null>(null);\n const listboxRef = useRef<HTMLDivElement | null>(null);\n const popupRef = useRef<HTMLElement | null>(null);\n const insidePressRef = useRef<Event | null>(null);\n const optionsRef = useRef(new Map<HTMLElement, ComboboxItemState>());\n const landingRef = useRef<Landing | null>(null);\n const wasOpenRef = useRef(false);\n const shownValuesRef = useRef<string[]>([]);\n // Whether the next highlight change is one to scroll to: a key's or a\n // landing's. The pointer's isn't, since the pointer is already on that\n // option, and scrolling would move the list out from under it.\n const scrollToHighlightRef = useRef(false);\n\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = inline || (controlledOpen ?? internalOpen);\n\n const [highlighted, setHighlighted] = useState<string | null>(null);\n const [typed, setTyped] = useState(false);\n // Whether a listbox with an option in it is on the page, which only the\n // page can say, so it's read once the open list has rendered.\n const [listShown, setListShown] = useState<boolean | null>(null);\n // Options register from their own effects, which needn't render the\n // component that holds this hook, so they ask for a render to be read.\n const [, refresh] = useReducer((n: number) => n + 1, 0);\n\n // Closing forgets the highlight, the typing and what was on the page,\n // during render, React's pattern for adjusting state when other state\n // changes.\n if (!open) {\n if (highlighted !== null) setHighlighted(null);\n if (typed) setTyped(false);\n if (listShown !== null) setListShown(null);\n }\n const expanded = open && (listShown ?? !deferred);\n\n /** The options on the page, in document order, groups included. */\n const shownOptions = useCallback((): ShownOption[] => {\n const listbox = listboxRef.current;\n if (!listbox) return [];\n const shown: ShownOption[] = [];\n listbox\n .querySelectorAll<HTMLElement>('[role=\"option\"]')\n .forEach(element => {\n // Only the Combobox's own options: an element given the role by\n // hand isn't one.\n const state = optionsRef.current.get(element);\n if (state) shown.push({ element, ...state });\n });\n return shown;\n }, []);\n\n const requestOpen = useCallback(\n (next: boolean, landing: Landing = 'open') => {\n if (inline || next === open) return;\n landingRef.current = next ? landing : null;\n onOpenChange?.(next);\n if (!isControlled) setInternalOpen(next);\n },\n [inline, open, onOpenChange, isControlled]\n );\n const close = useCallback(() => requestOpen(false), [requestOpen]);\n\n // After every render, read the list: whether it has options, and where\n // the highlight goes now. A highlighted option that has left takes the\n // highlight with it, or hands it to the first enabled one under\n // `keepFirst`, which also moves it there whenever the options change.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose\n useClientLayoutEffect(() => {\n const opened = open && !wasOpenRef.current;\n wasOpenRef.current = open;\n if (!open) {\n landingRef.current = null;\n shownValuesRef.current = [];\n return;\n }\n const options = shownOptions();\n const values = options.map(option => option.value);\n const changed = !sameValues(values, shownValuesRef.current);\n shownValuesRef.current = values;\n setListShown(values.length > 0);\n\n const landing = landingRef.current ?? (opened ? 'open' : null);\n landingRef.current = null;\n const enabled = options.filter(option => !option.disabled);\n const checked = enabled.find(option => option.checked)?.value;\n const first = enabled.length > 0 ? enabled[0].value : null;\n const last = enabled.length > 0 ? enabled[enabled.length - 1].value : null;\n let next = highlighted;\n if (landing === 'first') next = checked ?? first;\n else if (landing === 'last') next = checked ?? last;\n else if (landing === 'open') next = checked ?? (keepFirst ? first : null);\n else if (landing === 'typed') next = keepFirst ? first : null;\n else if (landing === 'none') next = null;\n else if (keepFirst && changed) next = first;\n // Without `keepFirst`, which moved it above, a highlighted option\n // that left takes the highlight with it.\n else if (next !== null && !values.includes(next)) next = null;\n // Only a change is set. StrictMode runs this twice on mount, and the\n // second run, with the landing used up, works out the rendered highlight\n // again, so it leaves the first run's landing in place.\n if (next !== highlighted) {\n scrollToHighlightRef.current = true;\n setHighlighted(next);\n }\n });\n\n // A browser scrolls to what has focus, not to an active descendant, so\n // the highlighted option is brought into view here, when a key or a\n // landing moved it there.\n useClientLayoutEffect(() => {\n if (highlighted === null || !scrollToHighlightRef.current) return;\n scrollToHighlightRef.current = false;\n const option = shownOptions().find(shown => shown.value === highlighted);\n option?.element.scrollIntoView?.({ block: 'nearest' });\n }, [highlighted, shownOptions]);\n\n // A press outside the field and the list closes it. The path is checked\n // rather than the target, so it works in a shadow root.\n useEffect(() => {\n const root = rootRef.current;\n if (!open || inline || !root) return undefined;\n const onPress = (event: PointerEvent) => {\n if (\n event === insidePressRef.current ||\n isEventInside(event, rootRef.current) ||\n isEventInside(event, popupRef.current)\n ) {\n return;\n }\n close();\n };\n // Its document, an iframe's when the tree renders into one.\n const doc = root.ownerDocument;\n doc.addEventListener('pointerdown', onPress);\n return () => doc.removeEventListener('pointerdown', onPress);\n }, [open, inline, close]);\n\n const submit = useCallback(\n (value: string) => {\n onSelect?.(value);\n if (!multiple) close();\n },\n [onSelect, multiple, close]\n );\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n // The caller's handler ran first and can keep a key, and an IME that's\n // composing owns every key.\n if (event.defaultPrevented || isComposing(event)) return;\n const { key, altKey } = event;\n\n if (!open) {\n if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {\n event.preventDefault();\n requestOpen(\n true,\n altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last'\n );\n }\n return;\n }\n\n switch (key) {\n case 'ArrowDown':\n case 'ArrowUp': {\n if (altKey) {\n if (key === 'ArrowUp' && !inline) {\n event.preventDefault();\n close();\n }\n return;\n }\n event.preventDefault();\n // Disabled options too, so a screen reader user finds them.\n const options = shownOptions();\n if (options.length === 0) return;\n const at = options.findIndex(option => option.value === highlighted);\n const step = key === 'ArrowDown' ? 1 : -1;\n const index =\n at === -1\n ? step === 1\n ? 0\n : options.length - 1\n : (at + step + options.length) % options.length;\n scrollToHighlightRef.current = true;\n setHighlighted(options[index].value);\n return;\n }\n case 'Enter': {\n const option = shownOptions().find(\n shown => shown.value === highlighted\n );\n if (option) {\n event.preventDefault();\n if (!option.disabled) submit(option.value);\n return;\n }\n // Nothing to pick: the key goes on, so a form submits.\n onSettle?.();\n close();\n return;\n }\n case 'Escape':\n if (inline) return;\n // It stops here only when there was something to close, so a\n // Popover or Modal around it keeps its own Escape otherwise. Nothing\n // stops it going on: a handler further out reads `defaultPrevented`.\n if (popupRef.current) event.preventDefault();\n close();\n return;\n case 'Tab':\n close();\n return;\n case 'Home':\n case 'End':\n case 'ArrowLeft':\n case 'ArrowRight':\n // The field's own keys, which take the field back from the list.\n setHighlighted(null);\n return;\n default:\n }\n };\n\n const onChange = () => {\n setTyped(true);\n if (open) {\n landingRef.current = 'typed';\n setHighlighted(null);\n refresh();\n } else {\n requestOpen(true, 'typed');\n }\n };\n\n const onClick = () => {\n requestOpen(true, 'open');\n };\n\n const onBlur = (event: React.FocusEvent<HTMLInputElement>) => {\n const next = event.relatedTarget as Node | null;\n if (\n next &&\n (rootRef.current?.contains(next) || popupRef.current?.contains(next))\n ) {\n return;\n }\n close();\n };\n\n const hover = (value: string) => {\n scrollToHighlightRef.current = false;\n setHighlighted(value);\n };\n\n const pick = (value: string) => {\n submit(value);\n // A press in the list keeps focus in the field, but a browser that\n // ignores that, or a tap, can leave it elsewhere.\n const input = inputRef.current;\n if (input && input.isConnected && getActiveElementInTree(input) !== input) {\n input.focus();\n }\n };\n\n const keepFocus = (event: React.MouseEvent) => event.preventDefault();\n\n const markInside = (event: React.PointerEvent) => {\n insidePressRef.current = event.nativeEvent;\n };\n\n const registerPopup = useCallback((element: HTMLElement) => {\n popupRef.current = element;\n return () => {\n popupRef.current = null;\n };\n }, []);\n\n const registerItem = useCallback(\n (element: HTMLElement, state: ComboboxItemState) => {\n const options = optionsRef.current;\n options.set(element, state);\n refresh();\n return () => {\n options.delete(element);\n refresh();\n };\n },\n []\n );\n\n return {\n open,\n expanded,\n highlighted,\n typed,\n listboxId,\n itemId,\n rootRef,\n inputRef,\n listboxRef,\n registerPopup,\n registerItem,\n onKeyDown,\n onChange,\n onClick,\n onBlur,\n hover,\n pick,\n keepFocus,\n markInside,\n };\n}\n"],"names":["isComposing"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useComboboxEngine.js","sources":["../../../src/helpers/useComboboxEngine.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport {\n useCallback,\n useEffect,\n useId,\n useReducer,\n useRef,\n useState,\n} from 'react';\nimport type React from 'react';\nimport { useClientLayoutEffect } from './useClientLayoutEffect';\nimport { getActiveElementInTree, isEventInside } from './shadowDom';\n\n/** What an item tells the engine about itself. */\nexport interface ComboboxItemState {\n /** What a pick hands back. Unique within the combobox. */\n value: string;\n /** Reachable with the arrows, never picked. */\n disabled: boolean;\n /** The value, or one of them. */\n checked: boolean;\n}\n\n/** An option on the page, with its element. */\ninterface ShownOption extends ComboboxItemState {\n element: HTMLElement;\n}\n\n/**\n * Where the highlight lands once an opened list has rendered, or once typing\n * has changed what's in it:\n *\n * - `first` and `last` (Down and Up Arrow): the checked option, otherwise the\n * first or last enabled one;\n * - `open` (a click, or any other way of opening): the checked option,\n * otherwise the first enabled one with `keepFirst`, otherwise nothing;\n * - `typed`: the first enabled option with `keepFirst`, otherwise nothing;\n * - `none` (Alt+Down Arrow): nothing.\n *\n * Only an enabled option takes it, so a disabled checked one is passed over.\n */\ntype Landing = 'first' | 'last' | 'open' | 'typed' | 'none';\n\n/** Options for `useComboboxEngine`. */\nexport interface ComboboxEngineOptions {\n /** Controlled open state. When set, opening and closing only report through `onOpenChange`. */\n open?: boolean;\n /** Initial open state when uncontrolled. */\n defaultOpen: boolean;\n /** Called with the open state asked for, when it differs from the current one. */\n onOpenChange?: (open: boolean) => void;\n /** Called with an item's `value` when it's picked. */\n onSelect?: (value: string) => void;\n /**\n * Called when Enter goes on to the form with nothing to pick, before the\n * list closes, so an owner that shows something other than the typing can\n * put it back before the form reads the field. An `inline` list doesn't\n * close, so this is the one word it gets.\n */\n onSettle?: () => void;\n /** Highlights the first enabled item whenever the list opens or its items change. */\n keepFirst: boolean;\n /** Several values: a pick keeps the list open. */\n multiple: boolean;\n /** The list is always on screen, so nothing opens or closes it. */\n inline: boolean;\n /**\n * The list's panel is portaled, so it has no server markup and shows only\n * from the commit after hydration. Until it has rendered, an open list\n * isn't taken to be on the page, so no `aria-controls` names it early.\n */\n deferred: boolean;\n}\n\n/** What `useComboboxEngine` hands the parts of a combobox. */\nexport interface ComboboxEngine {\n /** Whether the list is open. Always, with `inline`. */\n open: boolean;\n /**\n * The field's `aria-expanded`: open, with a listbox that has an option in\n * it on the page. Until the open list has rendered, it's taken to have\n * one, so server markup for an open list in place says it's expanded. A\n * `deferred` list isn't in server markup, so it's taken to have none.\n */\n expanded: boolean;\n /** The highlighted option's value, if any. */\n highlighted: string | null;\n /** Whether the user has typed since the list opened. */\n typed: boolean;\n /** The listbox's id. */\n listboxId: string;\n /** An item's id, which follows from its value. */\n itemId: (value: string) => string;\n /** For the element around the field and, unless it's portaled, the list. */\n rootRef: React.MutableRefObject<HTMLDivElement | null>;\n /** For the field. */\n inputRef: React.MutableRefObject<HTMLInputElement | null>;\n /** For the listbox. */\n listboxRef: React.MutableRefObject<HTMLDivElement | null>;\n /**\n * Records the floating panel while it's on screen, which makes it count as\n * inside, and makes an Escape that closes it stop there. Returns the\n * function that forgets it.\n */\n registerPopup: (element: HTMLElement) => () => void;\n /** Records an item's element. Returns the function that forgets it. */\n registerItem: (element: HTMLElement, state: ComboboxItemState) => () => void;\n /** The field's keys, to run after the caller's own handler. */\n onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;\n /** A change to the field's value: the user typed. */\n onChange: () => void;\n /** A click in the field. */\n onClick: () => void;\n /** Focus leaving the field. */\n onBlur: (event: React.FocusEvent<HTMLInputElement>) => void;\n /** The pointer moved over an enabled option. */\n hover: (value: string) => void;\n /** A click on an enabled option. Focus ends up in the field. */\n pick: (value: string) => void;\n /** For anything in the list: a press there doesn't take focus from the field. */\n keepFocus: (event: React.MouseEvent) => void;\n /**\n * For the floating panel: marks a press as inside, which React reports from\n * content the panel portals elsewhere as well.\n */\n markInside: (event: React.PointerEvent) => void;\n}\n\n/** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */\nconst isComposing = (event: React.KeyboardEvent): boolean =>\n event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;\n\nconst sameValues = (a: string[], b: string[]): boolean =>\n a.length === b.length && a.every((value, i) => value === b[i]);\n\n/**\n * The keys, the open state, the highlight and the ARIA of an editable\n * combobox whose list the app renders and filters (#1083). DOM focus stays\n * in the field the whole time: the highlight is `aria-activedescendant`, as\n * the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves\n * real focus, as a menu should.\n *\n * The options are read from the listbox in document order whenever they're\n * needed, so options in groups and options that come and go are found, and\n * the highlight is remembered by value, so it survives a re-render. The text\n * in the field and any value are the app's: a pick only calls\n * `onSelect`.\n *\n * @function useComboboxEngine\n * @param options - The open state, the pick callback and the shape.\n * @returns The state and handlers the parts wire up.\n */\nexport function useComboboxEngine({\n open: controlledOpen,\n defaultOpen,\n onOpenChange,\n onSelect,\n onSettle,\n keepFirst,\n multiple,\n inline,\n deferred,\n}: ComboboxEngineOptions): ComboboxEngine {\n const baseId = useId();\n const listboxId = `${baseId}-listbox`;\n const itemPrefix = `${baseId}-item-`;\n // The value goes into the id encoded, so a value with a space in it still\n // makes one id, not two.\n const itemId = useCallback(\n (value: string) => itemPrefix + encodeURIComponent(value),\n [itemPrefix]\n );\n\n const rootRef = useRef<HTMLDivElement | null>(null);\n const inputRef = useRef<HTMLInputElement | null>(null);\n const listboxRef = useRef<HTMLDivElement | null>(null);\n const popupRef = useRef<HTMLElement | null>(null);\n const insidePressRef = useRef<Event | null>(null);\n const optionsRef = useRef(new Map<HTMLElement, ComboboxItemState>());\n const landingRef = useRef<Landing | null>(null);\n const wasOpenRef = useRef(false);\n const shownValuesRef = useRef<string[]>([]);\n // Whether the next highlight change is one to scroll to: a key's or a\n // landing's. The pointer's isn't, since the pointer is already on that\n // option, and scrolling would move the list out from under it.\n const scrollToHighlightRef = useRef(false);\n\n const isControlled = controlledOpen !== undefined;\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = inline || (controlledOpen ?? internalOpen);\n\n const [highlighted, setHighlighted] = useState<string | null>(null);\n const [typed, setTyped] = useState(false);\n // Whether a listbox with an option in it is on the page, which only the\n // page can say, so it's read once the open list has rendered.\n const [listShown, setListShown] = useState<boolean | null>(null);\n // Options register from their own effects, which needn't render the\n // component that holds this hook, so they ask for a render to be read.\n const [, refresh] = useReducer((n: number) => n + 1, 0);\n\n // Closing forgets the highlight, the typing and what was on the page,\n // during render, React's pattern for adjusting state when other state\n // changes.\n if (!open) {\n if (highlighted !== null) setHighlighted(null);\n if (typed) setTyped(false);\n if (listShown !== null) setListShown(null);\n }\n const expanded = open && (listShown ?? !deferred);\n\n /** The options on the page, in document order, groups included. */\n const shownOptions = useCallback((): ShownOption[] => {\n const listbox = listboxRef.current;\n if (!listbox) return [];\n const shown: ShownOption[] = [];\n listbox\n .querySelectorAll<HTMLElement>('[role=\"option\"]')\n .forEach(element => {\n // Only the Combobox's own options: an element given the role by\n // hand isn't one.\n const state = optionsRef.current.get(element);\n if (state) shown.push({ element, ...state });\n });\n return shown;\n }, []);\n\n const requestOpen = useCallback(\n (next: boolean, landing: Landing = 'open') => {\n if (inline || next === open) return;\n landingRef.current = next ? landing : null;\n onOpenChange?.(next);\n if (!isControlled) setInternalOpen(next);\n },\n [inline, open, onOpenChange, isControlled]\n );\n const close = useCallback(() => requestOpen(false), [requestOpen]);\n\n // After every render, read the list: whether it has options, and where\n // the highlight goes now. A highlighted option that has left takes the\n // highlight with it, or hands it to the first enabled one under\n // `keepFirst`, which also moves it there whenever the options change.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose\n useClientLayoutEffect(() => {\n const opened = open && !wasOpenRef.current;\n wasOpenRef.current = open;\n if (!open) {\n landingRef.current = null;\n shownValuesRef.current = [];\n return;\n }\n const options = shownOptions();\n const values = options.map(option => option.value);\n const changed = !sameValues(values, shownValuesRef.current);\n shownValuesRef.current = values;\n setListShown(values.length > 0);\n\n const landing = landingRef.current ?? (opened ? 'open' : null);\n landingRef.current = null;\n const enabled = options.filter(option => !option.disabled);\n const checked = enabled.find(option => option.checked)?.value;\n const first = enabled.length > 0 ? enabled[0].value : null;\n const last = enabled.length > 0 ? enabled[enabled.length - 1].value : null;\n let next = highlighted;\n if (landing === 'first') next = checked ?? first;\n else if (landing === 'last') next = checked ?? last;\n else if (landing === 'open') next = checked ?? (keepFirst ? first : null);\n else if (landing === 'typed') next = keepFirst ? first : null;\n else if (landing === 'none') next = null;\n else if (keepFirst && changed) next = first;\n // Without `keepFirst`, which moved it above, a highlighted option\n // that left takes the highlight with it.\n else if (next !== null && !values.includes(next)) next = null;\n // Only a change is set. StrictMode runs this twice on mount, and the\n // second run, with the landing used up, works out the rendered highlight\n // again, so it leaves the first run's landing in place.\n if (next !== highlighted) {\n scrollToHighlightRef.current = true;\n setHighlighted(next);\n }\n });\n\n // A browser scrolls to what has focus, not to an active descendant, so\n // the highlighted option is brought into view here, when a key or a\n // landing moved it there.\n useClientLayoutEffect(() => {\n if (highlighted === null || !scrollToHighlightRef.current) return;\n scrollToHighlightRef.current = false;\n const option = shownOptions().find(shown => shown.value === highlighted);\n option?.element.scrollIntoView?.({ block: 'nearest' });\n }, [highlighted, shownOptions]);\n\n // A press outside the field and the list closes it. The path is checked\n // rather than the target, so it works in a shadow root.\n useEffect(() => {\n const root = rootRef.current;\n if (!open || inline || !root) return undefined;\n const onPress = (event: PointerEvent) => {\n if (\n event === insidePressRef.current ||\n isEventInside(event, rootRef.current) ||\n isEventInside(event, popupRef.current)\n ) {\n return;\n }\n close();\n };\n // Its document, an iframe's when the tree renders into one.\n const doc = root.ownerDocument;\n doc.addEventListener('pointerdown', onPress);\n return () => doc.removeEventListener('pointerdown', onPress);\n }, [open, inline, close]);\n\n const submit = useCallback(\n (value: string) => {\n onSelect?.(value);\n if (!multiple) close();\n },\n [onSelect, multiple, close]\n );\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n // The caller's handler ran first and can keep a key, and an IME that's\n // composing owns every key.\n if (event.defaultPrevented || isComposing(event)) return;\n const { key, altKey } = event;\n\n if (!open) {\n if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {\n event.preventDefault();\n requestOpen(\n true,\n altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last'\n );\n }\n return;\n }\n\n switch (key) {\n case 'ArrowDown':\n case 'ArrowUp': {\n if (altKey) {\n if (key === 'ArrowUp' && !inline) {\n event.preventDefault();\n close();\n }\n return;\n }\n event.preventDefault();\n // Disabled options too, so a screen reader user finds them.\n const options = shownOptions();\n if (options.length === 0) return;\n const at = options.findIndex(option => option.value === highlighted);\n const step = key === 'ArrowDown' ? 1 : -1;\n const index =\n at === -1\n ? step === 1\n ? 0\n : options.length - 1\n : (at + step + options.length) % options.length;\n scrollToHighlightRef.current = true;\n setHighlighted(options[index].value);\n return;\n }\n case 'Enter': {\n const option = shownOptions().find(\n shown => shown.value === highlighted\n );\n if (option) {\n event.preventDefault();\n if (!option.disabled) submit(option.value);\n return;\n }\n // Nothing to pick: the key goes on, so a form submits.\n onSettle?.();\n close();\n return;\n }\n case 'Escape':\n if (inline) return;\n // It stops here only when there was something to close, so a\n // Popover or Modal around it keeps its own Escape otherwise. Nothing\n // stops it going on: a handler further out reads `defaultPrevented`.\n if (popupRef.current) event.preventDefault();\n close();\n return;\n case 'Tab':\n close();\n return;\n case 'Home':\n case 'End':\n case 'ArrowLeft':\n case 'ArrowRight':\n // The field's own keys, which take the field back from the list.\n setHighlighted(null);\n return;\n default:\n }\n };\n\n const onChange = () => {\n setTyped(true);\n if (open) {\n landingRef.current = 'typed';\n setHighlighted(null);\n refresh();\n } else {\n requestOpen(true, 'typed');\n }\n };\n\n const onClick = () => {\n requestOpen(true, 'open');\n };\n\n const onBlur = (event: React.FocusEvent<HTMLInputElement>) => {\n const next = event.relatedTarget as Node | null;\n if (\n next &&\n (rootRef.current?.contains(next) || popupRef.current?.contains(next))\n ) {\n return;\n }\n close();\n };\n\n const hover = (value: string) => {\n scrollToHighlightRef.current = false;\n setHighlighted(value);\n };\n\n const pick = (value: string) => {\n submit(value);\n // A press in the list keeps focus in the field, but a browser that\n // ignores that, or a tap, can leave it elsewhere.\n const input = inputRef.current;\n if (input && input.isConnected && getActiveElementInTree(input) !== input) {\n input.focus();\n }\n };\n\n const keepFocus = (event: React.MouseEvent) => event.preventDefault();\n\n const markInside = (event: React.PointerEvent) => {\n insidePressRef.current = event.nativeEvent;\n };\n\n const registerPopup = useCallback((element: HTMLElement) => {\n popupRef.current = element;\n return () => {\n popupRef.current = null;\n };\n }, []);\n\n const registerItem = useCallback(\n (element: HTMLElement, state: ComboboxItemState) => {\n const options = optionsRef.current;\n options.set(element, state);\n refresh();\n return () => {\n options.delete(element);\n refresh();\n };\n },\n []\n );\n\n return {\n open,\n expanded,\n highlighted,\n typed,\n listboxId,\n itemId,\n rootRef,\n inputRef,\n listboxRef,\n registerPopup,\n registerItem,\n onKeyDown,\n onChange,\n onClick,\n onBlur,\n hover,\n pick,\n keepFocus,\n markInside,\n };\n}\n"],"names":["isComposing"],"mappings":";;;;;;AAAA;AAgIA;AACA,MAAMA,aAAW,GAAG,CAAC,KAA0B,KAC7C,KAAK,CAAC,WAAW,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,CAAC,OAAO,KAAK,GAAG;AAEpE,MAAM,UAAU,GAAG,CAAC,CAAW,EAAE,CAAW,KAC1C,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAEhE;;;;;;;;;;;;;;;;AAgBG;AACG,SAAU,iBAAiB,CAAC,EAChC,IAAI,EAAE,cAAc,EACpB,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,MAAM,EACN,QAAQ,GACc,EAAA;AACtB,IAAA,MAAM,MAAM,GAAG,KAAK,EAAE;AACtB,IAAA,MAAM,SAAS,GAAG,CAAA,EAAG,MAAM,UAAU;AACrC,IAAA,MAAM,UAAU,GAAG,CAAA,EAAG,MAAM,QAAQ;;;IAGpC,MAAM,MAAM,GAAG,WAAW,CACxB,CAAC,KAAa,KAAK,UAAU,GAAG,kBAAkB,CAAC,KAAK,CAAC,EACzD,CAAC,UAAU,CAAC,CACb;AAED,IAAA,MAAM,OAAO,GAAG,MAAM,CAAwB,IAAI,CAAC;AACnD,IAAA,MAAM,QAAQ,GAAG,MAAM,CAA0B,IAAI,CAAC;AACtD,IAAA,MAAM,UAAU,GAAG,MAAM,CAAwB,IAAI,CAAC;AACtD,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAqB,IAAI,CAAC;AACjD,IAAA,MAAM,cAAc,GAAG,MAAM,CAAe,IAAI,CAAC;IACjD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,GAAG,EAAkC,CAAC;AACpE,IAAA,MAAM,UAAU,GAAG,MAAM,CAAiB,IAAI,CAAC;AAC/C,IAAA,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC;AAChC,IAAA,MAAM,cAAc,GAAG,MAAM,CAAW,EAAE,CAAC;;;;AAI3C,IAAA,MAAM,oBAAoB,GAAG,MAAM,CAAC,KAAK,CAAC;AAE1C,IAAA,MAAM,YAAY,GAAG,cAAc,KAAK,SAAS;IACjD,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC;IAC7D,MAAM,IAAI,GAAG,MAAM,KAAK,cAAc,IAAI,YAAY,CAAC;IAEvD,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC;IACnE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;;;IAGzC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAiB,IAAI,CAAC;;;AAGhE,IAAA,MAAM,GAAG,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC,CAAS,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;;;;IAKvD,IAAI,CAAC,IAAI,EAAE;QACT,IAAI,WAAW,KAAK,IAAI;YAAE,cAAc,CAAC,IAAI,CAAC;AAC9C,QAAA,IAAI,KAAK;YAAE,QAAQ,CAAC,KAAK,CAAC;QAC1B,IAAI,SAAS,KAAK,IAAI;YAAE,YAAY,CAAC,IAAI,CAAC;IAC5C;IACA,MAAM,QAAQ,GAAG,IAAI,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC;;AAGjD,IAAA,MAAM,YAAY,GAAG,WAAW,CAAC,MAAoB;AACnD,QAAA,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO;AAClC,QAAA,IAAI,CAAC,OAAO;AAAE,YAAA,OAAO,EAAE;QACvB,MAAM,KAAK,GAAkB,EAAE;QAC/B;aACG,gBAAgB,CAAc,iBAAiB;aAC/C,OAAO,CAAC,OAAO,IAAG;;;YAGjB,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;AAC7C,YAAA,IAAI,KAAK;gBAAE,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;AAC9C,QAAA,CAAC,CAAC;AACJ,QAAA,OAAO,KAAK;IACd,CAAC,EAAE,EAAE,CAAC;IAEN,MAAM,WAAW,GAAG,WAAW,CAC7B,CAAC,IAAa,EAAE,OAAA,GAAmB,MAAM,KAAI;AAC3C,QAAA,IAAI,MAAM,IAAI,IAAI,KAAK,IAAI;YAAE;AAC7B,QAAA,UAAU,CAAC,OAAO,GAAG,IAAI,GAAG,OAAO,GAAG,IAAI;AAC1C,QAAA,YAAY,GAAG,IAAI,CAAC;AACpB,QAAA,IAAI,CAAC,YAAY;YAAE,eAAe,CAAC,IAAI,CAAC;IAC1C,CAAC,EACD,CAAC,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,CAAC,CAC3C;AACD,IAAA,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC;;;;;;IAOlE,qBAAqB,CAAC,MAAK;QACzB,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO;AAC1C,QAAA,UAAU,CAAC,OAAO,GAAG,IAAI;QACzB,IAAI,CAAC,IAAI,EAAE;AACT,YAAA,UAAU,CAAC,OAAO,GAAG,IAAI;AACzB,YAAA,cAAc,CAAC,OAAO,GAAG,EAAE;YAC3B;QACF;AACA,QAAA,MAAM,OAAO,GAAG,YAAY,EAAE;AAC9B,QAAA,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,CAAC;QAClD,MAAM,OAAO,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC;AAC3D,QAAA,cAAc,CAAC,OAAO,GAAG,MAAM;AAC/B,QAAA,YAAY,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;AAE/B,QAAA,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,KAAK,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAC9D,QAAA,UAAU,CAAC,OAAO,GAAG,IAAI;AACzB,QAAA,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;AAC1D,QAAA,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK;QAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI;QAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI;QAC1E,IAAI,IAAI,GAAG,WAAW;QACtB,IAAI,OAAO,KAAK,OAAO;AAAE,YAAA,IAAI,GAAG,OAAO,IAAI,KAAK;aAC3C,IAAI,OAAO,KAAK,MAAM;AAAE,YAAA,IAAI,GAAG,OAAO,IAAI,IAAI;aAC9C,IAAI,OAAO,KAAK,MAAM;AAAE,YAAA,IAAI,GAAG,OAAO,KAAK,SAAS,GAAG,KAAK,GAAG,IAAI,CAAC;aACpE,IAAI,OAAO,KAAK,OAAO;YAAE,IAAI,GAAG,SAAS,GAAG,KAAK,GAAG,IAAI;aACxD,IAAI,OAAO,KAAK,MAAM;YAAE,IAAI,GAAG,IAAI;aACnC,IAAI,SAAS,IAAI,OAAO;YAAE,IAAI,GAAG,KAAK;;;aAGtC,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,IAAI,GAAG,IAAI;;;;AAI7D,QAAA,IAAI,IAAI,KAAK,WAAW,EAAE;AACxB,YAAA,oBAAoB,CAAC,OAAO,GAAG,IAAI;YACnC,cAAc,CAAC,IAAI,CAAC;QACtB;AACF,IAAA,CAAC,CAAC;;;;IAKF,qBAAqB,CAAC,MAAK;AACzB,QAAA,IAAI,WAAW,KAAK,IAAI,IAAI,CAAC,oBAAoB,CAAC,OAAO;YAAE;AAC3D,QAAA,oBAAoB,CAAC,OAAO,GAAG,KAAK;AACpC,QAAA,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,WAAW,CAAC;AACxE,QAAA,MAAM,EAAE,OAAO,CAAC,cAAc,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AACxD,IAAA,CAAC,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;;;IAI/B,SAAS,CAAC,MAAK;AACb,QAAA,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO;AAC5B,QAAA,IAAI,CAAC,IAAI,IAAI,MAAM,IAAI,CAAC,IAAI;AAAE,YAAA,OAAO,SAAS;AAC9C,QAAA,MAAM,OAAO,GAAG,CAAC,KAAmB,KAAI;AACtC,YAAA,IACE,KAAK,KAAK,cAAc,CAAC,OAAO;AAChC,gBAAA,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC;gBACrC,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,EACtC;gBACA;YACF;AACA,YAAA,KAAK,EAAE;AACT,QAAA,CAAC;;AAED,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa;AAC9B,QAAA,GAAG,CAAC,gBAAgB,CAAC,aAAa,EAAE,OAAO,CAAC;QAC5C,OAAO,MAAM,GAAG,CAAC,mBAAmB,CAAC,aAAa,EAAE,OAAO,CAAC;IAC9D,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;AAEzB,IAAA,MAAM,MAAM,GAAG,WAAW,CACxB,CAAC,KAAa,KAAI;AAChB,QAAA,QAAQ,GAAG,KAAK,CAAC;AACjB,QAAA,IAAI,CAAC,QAAQ;AAAE,YAAA,KAAK,EAAE;IACxB,CAAC,EACD,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,CAC5B;AAED,IAAA,MAAM,SAAS,GAAG,CAAC,KAA4C,KAAI;;;AAGjE,QAAA,IAAI,KAAK,CAAC,gBAAgB,IAAIA,aAAW,CAAC,KAAK,CAAC;YAAE;AAClD,QAAA,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK;QAE7B,IAAI,CAAC,IAAI,EAAE;AACT,YAAA,IAAI,GAAG,KAAK,WAAW,KAAK,GAAG,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE;gBACzD,KAAK,CAAC,cAAc,EAAE;gBACtB,WAAW,CACT,IAAI,EACJ,MAAM,GAAG,MAAM,GAAG,GAAG,KAAK,WAAW,GAAG,OAAO,GAAG,MAAM,CACzD;YACH;YACA;QACF;QAEA,QAAQ,GAAG;AACT,YAAA,KAAK,WAAW;YAChB,KAAK,SAAS,EAAE;gBACd,IAAI,MAAM,EAAE;AACV,oBAAA,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,MAAM,EAAE;wBAChC,KAAK,CAAC,cAAc,EAAE;AACtB,wBAAA,KAAK,EAAE;oBACT;oBACA;gBACF;gBACA,KAAK,CAAC,cAAc,EAAE;;AAEtB,gBAAA,MAAM,OAAO,GAAG,YAAY,EAAE;AAC9B,gBAAA,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;oBAAE;AAC1B,gBAAA,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW,CAAC;AACpE,gBAAA,MAAM,IAAI,GAAG,GAAG,KAAK,WAAW,GAAG,CAAC,GAAG,EAAE;AACzC,gBAAA,MAAM,KAAK,GACT,EAAE,KAAK;sBACH,IAAI,KAAK;AACT,0BAAE;AACF,0BAAE,OAAO,CAAC,MAAM,GAAG;AACrB,sBAAE,CAAC,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM;AACnD,gBAAA,oBAAoB,CAAC,OAAO,GAAG,IAAI;gBACnC,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC;gBACpC;YACF;YACA,KAAK,OAAO,EAAE;AACZ,gBAAA,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC,IAAI,CAChC,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,WAAW,CACrC;gBACD,IAAI,MAAM,EAAE;oBACV,KAAK,CAAC,cAAc,EAAE;oBACtB,IAAI,CAAC,MAAM,CAAC,QAAQ;AAAE,wBAAA,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;oBAC1C;gBACF;;gBAEA,QAAQ,IAAI;AACZ,gBAAA,KAAK,EAAE;gBACP;YACF;AACA,YAAA,KAAK,QAAQ;AACX,gBAAA,IAAI,MAAM;oBAAE;;;;gBAIZ,IAAI,QAAQ,CAAC,OAAO;oBAAE,KAAK,CAAC,cAAc,EAAE;AAC5C,gBAAA,KAAK,EAAE;gBACP;AACF,YAAA,KAAK,KAAK;AACR,gBAAA,KAAK,EAAE;gBACP;AACF,YAAA,KAAK,MAAM;AACX,YAAA,KAAK,KAAK;AACV,YAAA,KAAK,WAAW;AAChB,YAAA,KAAK,YAAY;;gBAEf,cAAc,CAAC,IAAI,CAAC;gBACpB;;AAGN,IAAA,CAAC;IAED,MAAM,QAAQ,GAAG,MAAK;QACpB,QAAQ,CAAC,IAAI,CAAC;QACd,IAAI,IAAI,EAAE;AACR,YAAA,UAAU,CAAC,OAAO,GAAG,OAAO;YAC5B,cAAc,CAAC,IAAI,CAAC;AACpB,YAAA,OAAO,EAAE;QACX;aAAO;AACL,YAAA,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC;QAC5B;AACF,IAAA,CAAC;IAED,MAAM,OAAO,GAAG,MAAK;AACnB,QAAA,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC;AAC3B,IAAA,CAAC;AAED,IAAA,MAAM,MAAM,GAAG,CAAC,KAAyC,KAAI;AAC3D,QAAA,MAAM,IAAI,GAAG,KAAK,CAAC,aAA4B;AAC/C,QAAA,IACE,IAAI;AACJ,aAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,EACrE;YACA;QACF;AACA,QAAA,KAAK,EAAE;AACT,IAAA,CAAC;AAED,IAAA,MAAM,KAAK,GAAG,CAAC,KAAa,KAAI;AAC9B,QAAA,oBAAoB,CAAC,OAAO,GAAG,KAAK;QACpC,cAAc,CAAC,KAAK,CAAC;AACvB,IAAA,CAAC;AAED,IAAA,MAAM,IAAI,GAAG,CAAC,KAAa,KAAI;QAC7B,MAAM,CAAC,KAAK,CAAC;;;AAGb,QAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO;AAC9B,QAAA,IAAI,KAAK,IAAI,KAAK,CAAC,WAAW,IAAI,sBAAsB,CAAC,KAAK,CAAC,KAAK,KAAK,EAAE;YACzE,KAAK,CAAC,KAAK,EAAE;QACf;AACF,IAAA,CAAC;IAED,MAAM,SAAS,GAAG,CAAC,KAAuB,KAAK,KAAK,CAAC,cAAc,EAAE;AAErE,IAAA,MAAM,UAAU,GAAG,CAAC,KAAyB,KAAI;AAC/C,QAAA,cAAc,CAAC,OAAO,GAAG,KAAK,CAAC,WAAW;AAC5C,IAAA,CAAC;AAED,IAAA,MAAM,aAAa,GAAG,WAAW,CAAC,CAAC,OAAoB,KAAI;AACzD,QAAA,QAAQ,CAAC,OAAO,GAAG,OAAO;AAC1B,QAAA,OAAO,MAAK;AACV,YAAA,QAAQ,CAAC,OAAO,GAAG,IAAI;AACzB,QAAA,CAAC;IACH,CAAC,EAAE,EAAE,CAAC;IAEN,MAAM,YAAY,GAAG,WAAW,CAC9B,CAAC,OAAoB,EAAE,KAAwB,KAAI;AACjD,QAAA,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO;AAClC,QAAA,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC;AAC3B,QAAA,OAAO,EAAE;AACT,QAAA,OAAO,MAAK;AACV,YAAA,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;AACvB,YAAA,OAAO,EAAE;AACX,QAAA,CAAC;IACH,CAAC,EACD,EAAE,CACH;IAED,OAAO;QACL,IAAI;QACJ,QAAQ;QACR,WAAW;QACX,KAAK;QACL,SAAS;QACT,MAAM;QACN,OAAO;QACP,QAAQ;QACR,UAAU;QACV,aAAa;QACb,YAAY;QACZ,SAAS;QACT,QAAQ;QACR,OAAO;QACP,MAAM;QACN,KAAK;QACL,IAAI;QACJ,SAAS;QACT,UAAU;KACX;AACH;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useContextMenuTrigger.js","sources":["../../../src/helpers/useContextMenuTrigger.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useEffect, useRef } from 'react';\nimport type { MouseEvent, PointerEvent, TouchEvent } from 'react';\n\n/** A point in the viewport, in CSS pixels. */\nexport interface ContextMenuPoint {\n x: number;\n y: number;\n}\n\n/**\n * Options for `useContextMenuTrigger`.\n */\nexport interface UseContextMenuTriggerOptions {\n /**\n * Asks to open at `point`. `trigger` is the element the handlers are on,\n * for its document.\n */\n onOpen: (point: ContextMenuPoint, trigger: HTMLElement) => void;\n /** Leave the browser's own menu alone, and start no long press. */\n disabled: boolean;\n}\n\n/** Handlers for the trigger element. */\nexport interface ContextMenuTriggerHandlers {\n onContextMenu: (event: MouseEvent<HTMLElement>) => void;\n onPointerDown: (event: PointerEvent<HTMLElement>) => void;\n onPointerMove: (event: PointerEvent<HTMLElement>) => void;\n onPointerUp: (event: PointerEvent<HTMLElement>) => void;\n onPointerCancel: (event: PointerEvent<HTMLElement>) => void;\n onTouchEnd: (event: TouchEvent<HTMLElement>) => void;\n}\n\n/** How long a touch or a pen has to stay down to open the menu, in milliseconds. */\nexport const LONG_PRESS_DELAY = 500;\n\n/** How far a long press can move before it counts as something else, in CSS pixels. */\nexport const LONG_PRESS_TOLERANCE = 10;\n\n/** A touch or a pen held on the trigger. */\ninterface Press {\n pointerId: number;\n /** What it landed on, which its `contextmenu` goes to. */\n target: Element;\n x: number;\n y: number;\n timer?: ReturnType<typeof setTimeout>;\n /** A `contextmenu` arrived while it was down, so the menu opened. */\n opened: boolean;\n /** Stops listening for another pointer coming down in its document. */\n stop: () => void;\n}\n\n/**\n * The last press no trigger is to start a long press for: one a trigger\n * took, or another pointer coming down while a press is held. A press\n * reaches every trigger it bubbles through, the innermost first and all\n * before the next press, so this is how a trigger around another one knows\n * the inner one took it, and how a trigger a second finger lands on knows to\n * leave it.\n */\nlet claimed: Event | null = null;\n\n/** Whether the event happened in the element its handler is on, rather than in content portaled out of it. */\nconst inTrigger = (event: { currentTarget: Element; target: EventTarget }) =>\n event.currentTarget.contains(event.target as Node);\n\n/**\n * Where a `contextmenu` asks to open: at its point when that is on the\n * element it was fired at, which it is for the pointer, and at that\n * element's bottom-left corner otherwise, which is where the browser puts\n * its own menu for a key.\n */\nfunction contextMenuPoint(event: MouseEvent<HTMLElement>): ContextMenuPoint {\n const { clientX: x, clientY: y } = event;\n const rect = (event.target as Element).getBoundingClientRect();\n return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom\n ? { x, y }\n : { x: rect.left, y: rect.bottom };\n}\n\n/**\n * Decides when a context menu asks to open, and where. It renders nothing,\n * and knows nothing about ARIA or the menu.\n *\n * - **`contextmenu`:** a right-click, and Shift+F10 or the context menu key\n * where the browser fires the event for them, asks to open at the point\n * `contextMenuPoint` works out, and prevents the browser's own menu. Each\n * one asks again, so a right-click elsewhere on the trigger moves it.\n * - **Long press:** a touch or a pen held still on the trigger for\n * `LONG_PRESS_DELAY` fires a `contextmenu` at what it landed on, so it\n * takes the same path, handlers on the way included. Moving more than\n * `LONG_PRESS_TOLERANCE`, lifting, another pointer coming down anywhere in\n * the document, or `pointercancel`, which is the browser taking the\n * gesture for a scroll or a zoom, cancels it, and a mouse never starts\n * one. A pointer that comes down while another is held starts no long\n * press of its own, on this trigger or another, so two fingers on two rows\n * open nothing. While the finger is down, and only then, a listener on the\n * document hears other pointers coming down. Where the\n * browser fires `contextmenu` for a long press itself, the first of the two\n * opens it, and the end of a touch that opened it is prevented, so lifting\n * doesn't click what is under the finger.\n * - **Who opens:** a handler that calls `preventDefault()` on the\n * `contextmenu` before this one keeps it from asking, which is how a\n * trigger's own handler opts out and how the innermost of nested triggers\n * wins. A long press goes to the innermost trigger that takes it. Events\n * from content portaled out of the trigger, which React brings here\n * anyway, are ignored.\n * - **`disabled`:** nothing asks, and the browser's menu shows.\n *\n * Nothing it scheduled runs after the component unmounts.\n *\n * @function useContextMenuTrigger\n * @param options - How to ask to open, and whether to.\n * @returns Handlers for the trigger element.\n */\nexport function useContextMenuTrigger({\n onOpen,\n disabled,\n}: UseContextMenuTriggerOptions): ContextMenuTriggerHandlers {\n const pressRef = useRef<Press | null>(null);\n /** A long press opened the menu, and the touch that made it hasn't ended. */\n const heldRef = useRef(false);\n\n const cancel = () => {\n clearTimeout(pressRef.current?.timer);\n pressRef.current?.stop();\n pressRef.current = null;\n };\n\n useEffect(\n () => () => {\n clearTimeout(pressRef.current?.timer);\n pressRef.current?.stop();\n pressRef.current = null;\n },\n []\n );\n const isPress = (event: PointerEvent<HTMLElement>) =>\n pressRef.current?.pointerId === event.pointerId;\n\n return {\n onContextMenu: event => {\n if (disabled || event.defaultPrevented || !inTrigger(event)) return;\n event.preventDefault();\n const press = pressRef.current;\n if (press) {\n if (press.opened) return;\n clearTimeout(press.timer);\n press.opened = true;\n heldRef.current = true;\n }\n onOpen(contextMenuPoint(event), event.currentTarget);\n },\n onPointerDown: event => {\n const native = event.nativeEvent;\n if (\n disabled ||\n (event.pointerType !== 'touch' && event.pointerType !== 'pen') ||\n claimed === native ||\n !inTrigger(event)\n ) {\n return;\n }\n claimed = native;\n heldRef.current = false;\n const target = event.target as Element;\n const doc = target.ownerDocument;\n // Another pointer coming down anywhere makes it a pinch or a second\n // touch, not a long press. In the capture phase, so it's heard before\n // any trigger it lands on, which then leaves it. The press's own\n // pointerdown has passed the document already, so it isn't heard.\n const another = (other: Event) => {\n claimed = other;\n cancel();\n };\n doc.addEventListener('pointerdown', another, true);\n const press: Press = {\n pointerId: event.pointerId,\n target,\n x: event.clientX,\n y: event.clientY,\n opened: false,\n stop: () => doc.removeEventListener('pointerdown', another, true),\n };\n press.timer = setTimeout(() => {\n const view = press.target.ownerDocument.defaultView as typeof window;\n press.target.dispatchEvent(\n new view.MouseEvent('contextmenu', {\n bubbles: true,\n cancelable: true,\n composed: true,\n clientX: press.x,\n clientY: press.y,\n button: 2,\n })\n );\n }, LONG_PRESS_DELAY);\n pressRef.current = press;\n },\n onPointerMove: event => {\n const press = pressRef.current;\n if (!press || !isPress(event) || press.opened) return;\n const moved = Math.hypot(\n event.clientX - press.x,\n event.clientY - press.y\n );\n if (moved > LONG_PRESS_TOLERANCE) cancel();\n },\n onPointerUp: event => {\n if (isPress(event)) cancel();\n },\n onPointerCancel: event => {\n if (isPress(event)) cancel();\n },\n onTouchEnd: event => {\n if (!heldRef.current) return;\n heldRef.current = false;\n event.preventDefault();\n },\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useContextMenuTrigger.js","sources":["../../../src/helpers/useContextMenuTrigger.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport { useEffect, useRef } from 'react';\nimport type { MouseEvent, PointerEvent, TouchEvent } from 'react';\n\n/** A point in the viewport, in CSS pixels. */\nexport interface ContextMenuPoint {\n x: number;\n y: number;\n}\n\n/**\n * Options for `useContextMenuTrigger`.\n */\nexport interface UseContextMenuTriggerOptions {\n /**\n * Asks to open at `point`. `trigger` is the element the handlers are on,\n * for its document.\n */\n onOpen: (point: ContextMenuPoint, trigger: HTMLElement) => void;\n /** Leave the browser's own menu alone, and start no long press. */\n disabled: boolean;\n}\n\n/** Handlers for the trigger element. */\nexport interface ContextMenuTriggerHandlers {\n onContextMenu: (event: MouseEvent<HTMLElement>) => void;\n onPointerDown: (event: PointerEvent<HTMLElement>) => void;\n onPointerMove: (event: PointerEvent<HTMLElement>) => void;\n onPointerUp: (event: PointerEvent<HTMLElement>) => void;\n onPointerCancel: (event: PointerEvent<HTMLElement>) => void;\n onTouchEnd: (event: TouchEvent<HTMLElement>) => void;\n}\n\n/** How long a touch or a pen has to stay down to open the menu, in milliseconds. */\nexport const LONG_PRESS_DELAY = 500;\n\n/** How far a long press can move before it counts as something else, in CSS pixels. */\nexport const LONG_PRESS_TOLERANCE = 10;\n\n/** A touch or a pen held on the trigger. */\ninterface Press {\n pointerId: number;\n /** What it landed on, which its `contextmenu` goes to. */\n target: Element;\n x: number;\n y: number;\n timer?: ReturnType<typeof setTimeout>;\n /** A `contextmenu` arrived while it was down, so the menu opened. */\n opened: boolean;\n /** Stops listening for another pointer coming down in its document. */\n stop: () => void;\n}\n\n/**\n * The last press no trigger is to start a long press for: one a trigger\n * took, or another pointer coming down while a press is held. A press\n * reaches every trigger it bubbles through, the innermost first and all\n * before the next press, so this is how a trigger around another one knows\n * the inner one took it, and how a trigger a second finger lands on knows to\n * leave it.\n */\nlet claimed: Event | null = null;\n\n/** Whether the event happened in the element its handler is on, rather than in content portaled out of it. */\nconst inTrigger = (event: { currentTarget: Element; target: EventTarget }) =>\n event.currentTarget.contains(event.target as Node);\n\n/**\n * Where a `contextmenu` asks to open: at its point when that is on the\n * element it was fired at, which it is for the pointer, and at that\n * element's bottom-left corner otherwise, which is where the browser puts\n * its own menu for a key.\n */\nfunction contextMenuPoint(event: MouseEvent<HTMLElement>): ContextMenuPoint {\n const { clientX: x, clientY: y } = event;\n const rect = (event.target as Element).getBoundingClientRect();\n return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom\n ? { x, y }\n : { x: rect.left, y: rect.bottom };\n}\n\n/**\n * Decides when a context menu asks to open, and where. It renders nothing,\n * and knows nothing about ARIA or the menu.\n *\n * - **`contextmenu`:** a right-click, and Shift+F10 or the context menu key\n * where the browser fires the event for them, asks to open at the point\n * `contextMenuPoint` works out, and prevents the browser's own menu. Each\n * one asks again, so a right-click elsewhere on the trigger moves it.\n * - **Long press:** a touch or a pen held still on the trigger for\n * `LONG_PRESS_DELAY` fires a `contextmenu` at what it landed on, so it\n * takes the same path, handlers on the way included. Moving more than\n * `LONG_PRESS_TOLERANCE`, lifting, another pointer coming down anywhere in\n * the document, or `pointercancel`, which is the browser taking the\n * gesture for a scroll or a zoom, cancels it, and a mouse never starts\n * one. A pointer that comes down while another is held starts no long\n * press of its own, on this trigger or another, so two fingers on two rows\n * open nothing. While the finger is down, and only then, a listener on the\n * document hears other pointers coming down. Where the\n * browser fires `contextmenu` for a long press itself, the first of the two\n * opens it, and the end of a touch that opened it is prevented, so lifting\n * doesn't click what is under the finger.\n * - **Who opens:** a handler that calls `preventDefault()` on the\n * `contextmenu` before this one keeps it from asking, which is how a\n * trigger's own handler opts out and how the innermost of nested triggers\n * wins. A long press goes to the innermost trigger that takes it. Events\n * from content portaled out of the trigger, which React brings here\n * anyway, are ignored.\n * - **`disabled`:** nothing asks, and the browser's menu shows.\n *\n * Nothing it scheduled runs after the component unmounts.\n *\n * @function useContextMenuTrigger\n * @param options - How to ask to open, and whether to.\n * @returns Handlers for the trigger element.\n */\nexport function useContextMenuTrigger({\n onOpen,\n disabled,\n}: UseContextMenuTriggerOptions): ContextMenuTriggerHandlers {\n const pressRef = useRef<Press | null>(null);\n /** A long press opened the menu, and the touch that made it hasn't ended. */\n const heldRef = useRef(false);\n\n const cancel = () => {\n clearTimeout(pressRef.current?.timer);\n pressRef.current?.stop();\n pressRef.current = null;\n };\n\n useEffect(\n () => () => {\n clearTimeout(pressRef.current?.timer);\n pressRef.current?.stop();\n pressRef.current = null;\n },\n []\n );\n const isPress = (event: PointerEvent<HTMLElement>) =>\n pressRef.current?.pointerId === event.pointerId;\n\n return {\n onContextMenu: event => {\n if (disabled || event.defaultPrevented || !inTrigger(event)) return;\n event.preventDefault();\n const press = pressRef.current;\n if (press) {\n if (press.opened) return;\n clearTimeout(press.timer);\n press.opened = true;\n heldRef.current = true;\n }\n onOpen(contextMenuPoint(event), event.currentTarget);\n },\n onPointerDown: event => {\n const native = event.nativeEvent;\n if (\n disabled ||\n (event.pointerType !== 'touch' && event.pointerType !== 'pen') ||\n claimed === native ||\n !inTrigger(event)\n ) {\n return;\n }\n claimed = native;\n heldRef.current = false;\n const target = event.target as Element;\n const doc = target.ownerDocument;\n // Another pointer coming down anywhere makes it a pinch or a second\n // touch, not a long press. In the capture phase, so it's heard before\n // any trigger it lands on, which then leaves it. The press's own\n // pointerdown has passed the document already, so it isn't heard.\n const another = (other: Event) => {\n claimed = other;\n cancel();\n };\n doc.addEventListener('pointerdown', another, true);\n const press: Press = {\n pointerId: event.pointerId,\n target,\n x: event.clientX,\n y: event.clientY,\n opened: false,\n stop: () => doc.removeEventListener('pointerdown', another, true),\n };\n press.timer = setTimeout(() => {\n const view = press.target.ownerDocument.defaultView as typeof window;\n press.target.dispatchEvent(\n new view.MouseEvent('contextmenu', {\n bubbles: true,\n cancelable: true,\n composed: true,\n clientX: press.x,\n clientY: press.y,\n button: 2,\n })\n );\n }, LONG_PRESS_DELAY);\n pressRef.current = press;\n },\n onPointerMove: event => {\n const press = pressRef.current;\n if (!press || !isPress(event) || press.opened) return;\n const moved = Math.hypot(\n event.clientX - press.x,\n event.clientY - press.y\n );\n if (moved > LONG_PRESS_TOLERANCE) cancel();\n },\n onPointerUp: event => {\n if (isPress(event)) cancel();\n },\n onPointerCancel: event => {\n if (isPress(event)) cancel();\n },\n onTouchEnd: event => {\n if (!heldRef.current) return;\n heldRef.current = false;\n event.preventDefault();\n },\n };\n}\n"],"names":[],"mappings":"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"}
|