|
| 1 | +import type { JSX } from '@stencil/core'; |
| 2 | +import { Component, h, Method, Prop, State, Watch } from '@stencil/core'; |
| 3 | +import { KolButtonWcTag } from '../../core/component-names'; |
| 4 | +import { alignFloatingElements } from '../../utils/align-floating-elements'; |
| 5 | +import type { PopoverButtonProps, PopoverButtonStates } from '../../schema/components/popover-button'; |
| 6 | +import type { |
| 7 | + AccessKeyPropType, |
| 8 | + AlternativeButtonLinkRolePropType, |
| 9 | + AriaDescriptionPropType, |
| 10 | + ButtonCallbacksPropType, |
| 11 | + ButtonTypePropType, |
| 12 | + ButtonVariantPropType, |
| 13 | + CustomClassPropType, |
| 14 | + IconsPropType, |
| 15 | + LabelWithExpertSlotPropType, |
| 16 | + PopoverAlignPropType, |
| 17 | + ShortKeyPropType, |
| 18 | + StencilUnknown, |
| 19 | + Stringified, |
| 20 | + SyncValueBySelectorPropType, |
| 21 | + TooltipAlignPropType, |
| 22 | +} from '../../schema'; |
| 23 | +import { validatePopoverAlign } from '../../schema'; |
| 24 | + |
| 25 | +/** |
| 26 | + * @slot - The popover content. |
| 27 | + */ |
| 28 | +@Component({ |
| 29 | + tag: 'kol-popover-button-wc', |
| 30 | + shadow: false, |
| 31 | +}) |
| 32 | +// class implementing PopoverButtonProps and not API because we don't want to repeat the entire state and validation for button props |
| 33 | +export class KolPopoverButton implements PopoverButtonProps { |
| 34 | + private refButton?: HTMLKolButtonWcElement; |
| 35 | + private refPopover?: HTMLDivElement; |
| 36 | + |
| 37 | + @State() public state: PopoverButtonStates = { |
| 38 | + _label: '', |
| 39 | + _popoverAlign: 'bottom', |
| 40 | + }; |
| 41 | + @State() private justClosed = false; |
| 42 | + |
| 43 | + /** |
| 44 | + * Hides the popover programmatically by calling the native hidePopover method. |
| 45 | + */ |
| 46 | + @Method() |
| 47 | + // eslint-disable-next-line @typescript-eslint/require-await |
| 48 | + public async hidePopover() { |
| 49 | + void this.refPopover?.hidePopover(); |
| 50 | + } |
| 51 | + |
| 52 | + /* Regarding type issue see https://github.com/microsoft/TypeScript/issues/54864 */ |
| 53 | + private handleBeforeToggle(event: Event) { |
| 54 | + if ((event as ToggleEvent).newState === 'closed') { |
| 55 | + this.justClosed = true; |
| 56 | + |
| 57 | + setTimeout(() => { |
| 58 | + // Reset the flag after the event loop tick. |
| 59 | + this.justClosed = false; |
| 60 | + }, 10); // timeout of 0 should be sufficient but doesn't work in Safari Mobile (needs further investigation). |
| 61 | + } else if (this.refPopover) { |
| 62 | + /** |
| 63 | + * Avoid "flicker" by hiding the element until the position is set in the `toggle` event handler. `alignFloatingElements` is responsible for setting the visibility back to 'visible'. |
| 64 | + */ |
| 65 | + this.refPopover.style.visibility = 'hidden'; |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + private handleToggle(event: Event) { |
| 70 | + if ((event as ToggleEvent).newState === 'open' && this.refPopover && this.refButton) { |
| 71 | + void alignFloatingElements({ |
| 72 | + align: this.state._popoverAlign, |
| 73 | + floatingElement: this.refPopover, |
| 74 | + referenceElement: this.refButton, |
| 75 | + }); |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + private handleButtonClick() { |
| 80 | + // If the popover was just closed by native behavior, do nothing (and let it stay closed). |
| 81 | + if (!this.justClosed) { |
| 82 | + this.refPopover?.togglePopover(); |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + public componentDidRender() { |
| 87 | + this.refPopover?.addEventListener('toggle', this.handleToggle.bind(this)); |
| 88 | + this.refPopover?.addEventListener('beforetoggle', this.handleBeforeToggle.bind(this)); |
| 89 | + } |
| 90 | + |
| 91 | + public disconnectedCallback() { |
| 92 | + this.refPopover?.removeEventListener('toggle', this.handleToggle.bind(this)); |
| 93 | + this.refPopover?.removeEventListener('beforetoggle', this.handleBeforeToggle.bind(this)); |
| 94 | + } |
| 95 | + |
| 96 | + public render(): JSX.Element { |
| 97 | + return ( |
| 98 | + <div class="kol-popover-button"> |
| 99 | + <KolButtonWcTag |
| 100 | + _accessKey={this._accessKey} |
| 101 | + _ariaControls={this._ariaControls} |
| 102 | + _ariaDescription={this._ariaDescription} |
| 103 | + _ariaExpanded={this._ariaExpanded} |
| 104 | + _ariaSelected={this._ariaSelected} |
| 105 | + _customClass={this._customClass} |
| 106 | + _disabled={this._disabled} |
| 107 | + _hideLabel={this._hideLabel} |
| 108 | + _icons={this._icons} |
| 109 | + _id={this._id} |
| 110 | + _label={this._label} |
| 111 | + _name={this._name} |
| 112 | + _on={this._on} |
| 113 | + _role={this._role} |
| 114 | + _shortKey={this._shortKey} |
| 115 | + _syncValueBySelector={this._syncValueBySelector} |
| 116 | + _tabIndex={this._tabIndex} |
| 117 | + _tooltipAlign={this._tooltipAlign} |
| 118 | + _type={this._type} |
| 119 | + _value={this._value} |
| 120 | + _variant={this._variant} |
| 121 | + data-testid="popover-button" |
| 122 | + class="kol-popover-button__button" |
| 123 | + ref={(element) => (this.refButton = element)} |
| 124 | + onClick={this.handleButtonClick.bind(this)} |
| 125 | + > |
| 126 | + <slot name="expert" slot="expert"></slot> |
| 127 | + </KolButtonWcTag> |
| 128 | + |
| 129 | + <div ref={(element) => (this.refPopover = element)} data-testid="popover-content" popover="auto" id="popover" class="kol-popover-button__popover"> |
| 130 | + <slot /> |
| 131 | + </div> |
| 132 | + </div> |
| 133 | + ); |
| 134 | + } |
| 135 | + |
| 136 | + /** |
| 137 | + * Defines which key combination can be used to trigger or focus the interactive element of the component. |
| 138 | + */ |
| 139 | + @Prop() public _accessKey?: AccessKeyPropType; |
| 140 | + |
| 141 | + /** |
| 142 | + * Defines which elements are controlled by this component. (https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls) |
| 143 | + */ |
| 144 | + @Prop() public _ariaControls?: string; |
| 145 | + |
| 146 | + /** |
| 147 | + * Defines the value for the aria-description attribute. |
| 148 | + */ |
| 149 | + @Prop() public _ariaDescription?: AriaDescriptionPropType; |
| 150 | + |
| 151 | + /** |
| 152 | + * Defines whether the interactive element of the component expanded something. (https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded) |
| 153 | + */ |
| 154 | + @Prop() public _ariaExpanded?: boolean; |
| 155 | + |
| 156 | + /** |
| 157 | + * Defines whether the interactive element of the component is selected (e.g. role=tab). (https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-selected) |
| 158 | + */ |
| 159 | + @Prop() public _ariaSelected?: boolean; |
| 160 | + |
| 161 | + /** |
| 162 | + * Defines the custom class attribute if _variant="custom" is set. |
| 163 | + */ |
| 164 | + @Prop() public _customClass?: CustomClassPropType; |
| 165 | + |
| 166 | + /** |
| 167 | + * Makes the element not focusable and ignore all events. |
| 168 | + */ |
| 169 | + @Prop() public _disabled?: boolean = false; |
| 170 | + |
| 171 | + /** |
| 172 | + * Hides the caption by default and displays the caption text with a tooltip when the |
| 173 | + * interactive element is focused or the mouse is over it. |
| 174 | + * @TODO: Change type back to `HideLabelPropType` after Stencil#4663 has been resolved. |
| 175 | + */ |
| 176 | + @Prop() public _hideLabel?: boolean = false; |
| 177 | + |
| 178 | + /** |
| 179 | + * Defines the icon classnames (e.g. `_icons="fa-solid fa-user"`). |
| 180 | + */ |
| 181 | + @Prop() public _icons?: IconsPropType; |
| 182 | + |
| 183 | + /** |
| 184 | + * Defines the internal ID of the primary component element. |
| 185 | + */ |
| 186 | + @Prop() public _id?: string; |
| 187 | + |
| 188 | + /** |
| 189 | + * Defines the visible or semantic label of the component (e.g. aria-label, label, headline, caption, summary, etc.). Set to `false` to enable the expert slot. |
| 190 | + */ |
| 191 | + @Prop() public _label!: LabelWithExpertSlotPropType; |
| 192 | + |
| 193 | + /** |
| 194 | + * Defines the technical name of an input field. |
| 195 | + */ |
| 196 | + @Prop() public _name?: string; |
| 197 | + |
| 198 | + /** |
| 199 | + * Defines the callback functions for button events. |
| 200 | + */ |
| 201 | + @Prop() public _on?: ButtonCallbacksPropType<StencilUnknown>; |
| 202 | + |
| 203 | + /** |
| 204 | + * Defines where to show the Popover preferably: top, right, bottom or left. |
| 205 | + */ |
| 206 | + @Prop() public _popoverAlign?: PopoverAlignPropType = 'bottom'; |
| 207 | + |
| 208 | + /** |
| 209 | + * Defines the role of the components primary element. |
| 210 | + */ |
| 211 | + @Prop() public _role?: AlternativeButtonLinkRolePropType; |
| 212 | + |
| 213 | + /** |
| 214 | + * Adds a visual short key hint to the component. |
| 215 | + */ |
| 216 | + @Prop() public _shortKey?: ShortKeyPropType; |
| 217 | + |
| 218 | + /** |
| 219 | + * Selector for synchronizing the value with another input element. |
| 220 | + * @internal |
| 221 | + */ |
| 222 | + @Prop() public _syncValueBySelector?: SyncValueBySelectorPropType; |
| 223 | + |
| 224 | + /** |
| 225 | + * Defines which tab-index the primary element of the component has. (https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) |
| 226 | + */ |
| 227 | + @Prop() public _tabIndex?: number; |
| 228 | + |
| 229 | + /** |
| 230 | + * Defines where to show the Tooltip preferably: top, right, bottom or left. |
| 231 | + */ |
| 232 | + @Prop() public _tooltipAlign?: TooltipAlignPropType = 'top'; |
| 233 | + |
| 234 | + /** |
| 235 | + * Defines either the type of the component or of the components interactive element. |
| 236 | + */ |
| 237 | + @Prop() public _type?: ButtonTypePropType = 'button'; |
| 238 | + |
| 239 | + /** |
| 240 | + * Defines the value that the button emits on click. |
| 241 | + */ |
| 242 | + @Prop() public _value?: Stringified<StencilUnknown>; |
| 243 | + |
| 244 | + /** |
| 245 | + * Defines which variant should be used for presentation. |
| 246 | + */ |
| 247 | + @Prop() public _variant?: ButtonVariantPropType = 'normal'; |
| 248 | + |
| 249 | + @Watch('_popoverAlign') |
| 250 | + public validatePopoverAlign(value?: PopoverAlignPropType): void { |
| 251 | + validatePopoverAlign(this, value); |
| 252 | + } |
| 253 | + |
| 254 | + public componentWillLoad() { |
| 255 | + this.validatePopoverAlign(this._popoverAlign); |
| 256 | + } |
| 257 | +} |
0 commit comments