lapikit 0.7.9 → 0.8.1

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.
Files changed (28) hide show
  1. package/dist/components/btn/btn.svelte +25 -4
  2. package/dist/components/btn/btn.types.d.ts +3 -0
  3. package/dist/components/card/card.svelte +6 -1
  4. package/dist/components/chip/chip.svelte +25 -4
  5. package/dist/components/chip/chip.types.d.ts +3 -0
  6. package/dist/components/dropdown/dropdown.svelte +120 -41
  7. package/dist/components/dropdown/dropdown.svelte.d.ts +1 -1
  8. package/dist/components/dropdown/dropdown.types.d.ts +19 -8
  9. package/dist/components/field/field.svelte +22 -0
  10. package/dist/components/field/field.types.d.ts +1 -0
  11. package/dist/components/icon/icon.svelte +11 -3
  12. package/dist/components/icon/icon.types.d.ts +3 -0
  13. package/dist/components/index.d.ts +3 -3
  14. package/dist/components/list/list.context.d.ts +7 -0
  15. package/dist/components/list/list.context.js +4 -0
  16. package/dist/components/list/list.svelte +18 -4
  17. package/dist/components/list/modules/list-item.svelte +68 -33
  18. package/dist/components/popover/popover.svelte +57 -10
  19. package/dist/components/popover/popover.types.d.ts +10 -4
  20. package/dist/components/textfield/textfield.svelte +5 -12
  21. package/dist/components/textfield/textfield.types.d.ts +1 -5
  22. package/dist/components/tooltip/tooltip-action.svelte.d.ts +9 -0
  23. package/dist/components/tooltip/tooltip-action.svelte.js +159 -0
  24. package/dist/components/tooltip/tooltip.svelte +71 -56
  25. package/dist/components/tooltip/tooltip.svelte.d.ts +4 -2
  26. package/dist/components/tooltip/tooltip.types.d.ts +28 -2
  27. package/dist/labs/sheet/sheet.svelte +6 -1
  28. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { useComponentAttrs } from '../../utils';
3
+ import { setListContext } from './list.context.js';
3
4
  import type { ListProps } from './list.types.js';
4
5
 
5
6
  let {
@@ -22,6 +23,16 @@
22
23
  let attrs = $derived(
23
24
  useComponentAttrs('kit-list', { className, sClass, styleAttr, sStyle, color }, rest)
24
25
  );
26
+ let role = $derived(nav ? 'navigation' : is === 'ul' ? undefined : 'list');
27
+ let customRole = $derived(attrs.rest.role as string | undefined);
28
+
29
+ setListContext({
30
+ get item() {
31
+ if (customRole) return customRole === 'list' ? 'listitem' : undefined;
32
+ if (is === 'ul') return 'li';
33
+ return role === 'list' ? 'listitem' : undefined;
34
+ }
35
+ });
25
36
  </script>
26
37
 
27
38
  <svelte:element
@@ -34,7 +45,7 @@
34
45
  data-density={density}
35
46
  data-rounded={rounded}
36
47
  data-nav={nav}
37
- role={nav ? 'navigation' : is === 'ul' ? undefined : 'list'}
48
+ {role}
38
49
  {...attrs.rest}
39
50
  >
40
51
  {@render children?.()}
@@ -61,15 +72,18 @@
61
72
  .kit-list[data-nav=true] :global(.kit-list-item) {
62
73
  border-radius: var(--kit-list-item-radius);
63
74
  }
64
- .kit-list:not([data-nav=true]) :global(.kit-list-item:first-child) {
75
+ .kit-list:not([data-nav=true]) :global(.kit-list-item:first-child:not(.kit-list-item-wrapper > *)),
76
+ .kit-list:not([data-nav=true]) :global(.kit-list-item-wrapper:first-child > .kit-list-item) {
65
77
  border-start-start-radius: var(--kit-list-item-radius);
66
78
  border-start-end-radius: var(--kit-list-item-radius);
67
79
  }
68
- .kit-list:not([data-nav=true]) :global(.kit-list-item:last-child) {
80
+ .kit-list:not([data-nav=true]) :global(.kit-list-item:last-child:not(.kit-list-item-wrapper > *)),
81
+ .kit-list:not([data-nav=true]) :global(.kit-list-item-wrapper:last-child > .kit-list-item) {
69
82
  border-end-start-radius: var(--kit-list-item-radius);
70
83
  border-end-end-radius: var(--kit-list-item-radius);
71
84
  }
72
- .kit-list:not([data-nav=true]) :global(.kit-list-item:not(:first-child, :last-child)) {
85
+ .kit-list:not([data-nav=true]) :global(.kit-list-item:not(:first-child, :last-child)),
86
+ .kit-list:not([data-nav=true]) :global(.kit-list-item-wrapper:not(:first-child, :last-child) > .kit-list-item) {
73
87
  --kit-list-item-radius: 0;
74
88
  }
75
89
  .kit-list:is([data-variant=outline], [data-variant=surface]) :global(.kit-list-item::before) {
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { ripple } from '../../../animations';
3
3
  import { useComponentAttrs, useIsInteractive } from '../../../utils';
4
+ import { getListContext } from '../list.context.js';
4
5
  import type { ListItemProps } from '../list.types.js';
5
6
 
6
7
  let {
7
8
  ref = $bindable(),
8
- is = 'div',
9
+ is,
9
10
  children = undefined,
10
11
  class: className,
11
12
  style: styleAttr,
@@ -29,42 +30,67 @@
29
30
  rest
30
31
  )
31
32
  );
33
+ const list = getListContext();
34
+
32
35
  let tag = $derived(
33
- href ? 'a' : is === 'div' && typeof attrs.rest.onclick === 'function' ? 'button' : is
36
+ href
37
+ ? 'a'
38
+ : (is ?? 'div') === 'div' && typeof attrs.rest.onclick === 'function'
39
+ ? 'button'
40
+ : (is ?? (list?.item === 'li' ? 'li' : 'div'))
34
41
  );
35
42
  let isInteractive = $derived(interactive ?? useIsInteractive(rest, tag, ['a', 'button']));
43
+ let wrapperTag = $derived.by(() => {
44
+ if (list?.item === 'li') return tag === 'li' ? undefined : 'li';
45
+ if (list?.item === 'listitem') return tag === 'a' || tag === 'button' ? 'div' : undefined;
46
+ return undefined;
47
+ });
36
48
  </script>
37
49
 
38
- <svelte:element
39
- this={tag}
40
- bind:this={ref}
41
- class={attrs.class}
42
- style={attrs.style}
43
- href={href && !disabled ? href : undefined}
44
- type={tag === 'button' ? 'button' : undefined}
45
- disabled={tag === 'button' ? disabled : undefined}
46
- tabindex={href && disabled ? -1 : undefined}
47
- role={tag === 'div' ? 'listitem' : undefined}
48
- data-active={active}
49
- data-disabled={disabled}
50
- data-interactive={isInteractive}
51
- aria-disabled={disabled || undefined}
52
- use:ripple={{
53
- component: 'list-item',
54
- disabled: noRipple || disabled || !isInteractive
55
- }}
56
- {...attrs.rest}
57
- >
58
- {#if prepend}
59
- <span class="kit-list-item__prepend">{@render prepend()}</span>
60
- {/if}
61
-
62
- <span class="kit-list-item__content">{@render children?.()}</span>
63
-
64
- {#if append}
65
- <span class="kit-list-item__append">{@render append()}</span>
66
- {/if}
67
- </svelte:element>
50
+ {#snippet item()}
51
+ <svelte:element
52
+ this={tag}
53
+ bind:this={ref}
54
+ class={attrs.class}
55
+ style={attrs.style}
56
+ href={href && !disabled ? href : undefined}
57
+ type={tag === 'button' ? 'button' : undefined}
58
+ disabled={tag === 'button' ? disabled : undefined}
59
+ tabindex={href && disabled ? -1 : undefined}
60
+ role={tag === 'div' && list?.item === 'listitem' ? 'listitem' : undefined}
61
+ data-active={active}
62
+ data-disabled={disabled}
63
+ data-interactive={isInteractive}
64
+ aria-disabled={disabled || undefined}
65
+ use:ripple={{
66
+ component: 'list-item',
67
+ disabled: noRipple || disabled || !isInteractive
68
+ }}
69
+ {...attrs.rest}
70
+ >
71
+ {#if prepend}
72
+ <span class="kit-list-item__prepend">{@render prepend()}</span>
73
+ {/if}
74
+
75
+ <span class="kit-list-item__content">{@render children?.()}</span>
76
+
77
+ {#if append}
78
+ <span class="kit-list-item__append">{@render append()}</span>
79
+ {/if}
80
+ </svelte:element>
81
+ {/snippet}
82
+
83
+ {#if wrapperTag}
84
+ <svelte:element
85
+ this={wrapperTag}
86
+ class="kit-list-item-wrapper"
87
+ role={wrapperTag === 'div' ? 'listitem' : undefined}
88
+ >
89
+ {@render item()}
90
+ </svelte:element>
91
+ {:else}
92
+ {@render item()}
93
+ {/if}
68
94
 
69
95
  <style>:where(.kit-list-item) {
70
96
  --kit-list-item-color: initial;
@@ -96,6 +122,10 @@
96
122
  --kit-list-item-on-color: var(--kit-list-on-color);
97
123
  }
98
124
 
125
+ .kit-list-item-wrapper {
126
+ display: contents;
127
+ }
128
+
99
129
  .kit-list-item {
100
130
  position: relative;
101
131
  display: flex;
@@ -166,7 +196,7 @@ button.kit-list-item {
166
196
  color: var(--kit-list-item-pressed-fg, var(--kit-list-item-fg));
167
197
  }
168
198
 
169
- .kit-list-item:not([data-disabled=true]):focus-visible {
199
+ .kit-list-item:is(:not([data-disabled=true]), [data-disabled-focusable=true]):focus-visible {
170
200
  outline: 2px solid var(--kit-color-focus);
171
201
  outline-offset: 2px;
172
202
  }
@@ -175,4 +205,9 @@ button.kit-list-item {
175
205
  opacity: var(--kit-disabled-opacity, 0.55);
176
206
  pointer-events: none;
177
207
  user-select: none;
208
+ }
209
+
210
+ .kit-list-item[data-disabled-focusable=true] {
211
+ pointer-events: auto;
212
+ cursor: not-allowed;
178
213
  }</style>
@@ -1,11 +1,13 @@
1
1
  <script lang="ts">
2
+ import { DEV } from 'esm-env';
2
3
  import { clickOutside, floating, useComponentAttrs, useElevation } from '../../utils';
3
- import type { ModelPopoverProps, PopoverProps } from './popover.types.js';
4
+ import type { PopoverProps, PopoverTriggerProps } from './popover.types.js';
4
5
 
5
6
  let {
6
7
  ref = $bindable(),
7
8
  open = $bindable(false),
8
9
  children,
10
+ trigger,
9
11
  activator,
10
12
  rounded = 'md',
11
13
  position = 'bottom',
@@ -19,31 +21,73 @@
19
21
  ...rest
20
22
  }: PopoverProps = $props();
21
23
 
24
+ $effect(() => {
25
+ if (DEV && activator)
26
+ console.warn('[lapikit] kit:popover: activator is removed, use the trigger snippet');
27
+ });
28
+
29
+ const popoverId = $props.id();
30
+
22
31
  let attrs = $derived(
23
32
  useComponentAttrs('kit-popover-content', { className, sClass, styleAttr, sStyle, color }, rest)
24
33
  );
25
34
  let elevationState = $derived(useElevation(elevation));
26
35
 
27
36
  let contentRef = $state<HTMLElement | null>(null);
28
- let activatorRef = $state<HTMLElement | null>(null);
37
+ let triggerRef = $state<HTMLElement | null>(null);
29
38
 
30
- const model: ModelPopoverProps = {
31
- toggle: (element) => {
32
- if (element === null) return;
33
- activatorRef = element;
39
+ const focusableSelector =
40
+ 'a[href], button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
41
+
42
+ const close = (restoreFocus = false) => {
43
+ open = false;
44
+ if (restoreFocus) triggerRef?.focus();
45
+ };
46
+
47
+ let triggerProps: PopoverTriggerProps = $derived({
48
+ 'aria-haspopup': 'dialog',
49
+ 'aria-expanded': open,
50
+ 'aria-controls': popoverId,
51
+ onclick: (event: MouseEvent) => {
52
+ triggerRef = event.currentTarget as HTMLElement;
34
53
  open = !open;
35
54
  }
55
+ });
56
+
57
+ // focus leaves the popover for another element of the page
58
+ const onFocusOut = (event: FocusEvent) => {
59
+ const next = event.relatedTarget as Node | null;
60
+ if (!next || contentRef?.contains(next) || triggerRef?.contains(next)) return;
61
+ close();
36
62
  };
37
63
 
38
64
  $effect(() => {
39
65
  ref = contentRef;
40
66
  });
41
67
 
68
+ $effect(() => {
69
+ if (open && !triggerRef?.isConnected) {
70
+ triggerRef = document.querySelector<HTMLElement>(
71
+ `[aria-controls="${CSS.escape(popoverId)}"]`
72
+ );
73
+ }
74
+ });
75
+
76
+ $effect(() => {
77
+ if (!open || !contentRef) return;
78
+ const content = contentRef;
79
+ const frame = requestAnimationFrame(() => {
80
+ const first = content.querySelector<HTMLElement>(focusableSelector);
81
+ (first ?? content).focus();
82
+ });
83
+ return () => cancelAnimationFrame(frame);
84
+ });
85
+
42
86
  $effect(() => {
43
87
  if (!open) return;
44
88
 
45
89
  const onKeydown = (event: KeyboardEvent) => {
46
- if (event.key === 'Escape') open = false;
90
+ if (event.key === 'Escape') close(true);
47
91
  };
48
92
 
49
93
  document.addEventListener('keydown', onKeydown);
@@ -51,22 +95,25 @@
51
95
  });
52
96
  </script>
53
97
 
54
- {@render activator?.(model)}
98
+ {@render trigger?.(triggerProps)}
55
99
 
56
100
  {#if open}
57
101
  <div
58
102
  bind:this={contentRef}
103
+ id={popoverId}
59
104
  popover="manual"
60
105
  class={attrs.class}
61
106
  style={attrs.style}
62
107
  role="dialog"
108
+ tabindex="-1"
63
109
  data-rounded={rounded}
64
110
  data-density={density}
65
111
  data-elevation={elevationState.base}
66
112
  data-elevation-hover={elevationState.hover}
67
113
  data-elevation-active={elevationState.active}
68
- use:floating={{ anchor: activatorRef, side: position, offset: 6 }}
69
- use:clickOutside={{ exclude: [contentRef, activatorRef], onClose: () => (open = false) }}
114
+ onfocusout={onFocusOut}
115
+ use:floating={{ anchor: triggerRef, side: position, offset: 6 }}
116
+ use:clickOutside={{ exclude: [contentRef, triggerRef], onClose: () => close() }}
70
117
  {...attrs.rest}
71
118
  >
72
119
  {@render children?.()}
@@ -1,15 +1,21 @@
1
1
  import type { ColorProps, Component, ElevationProps, RoundedType } from '../../@types';
2
2
  import type { Snippet } from 'svelte';
3
- export type ModelPopoverProps = {
4
- toggle: (element: HTMLElement | null) => void;
5
- };
3
+ /** The attributes to spread on the element that opens the popover */
4
+ export interface PopoverTriggerProps {
5
+ 'aria-haspopup': 'dialog';
6
+ 'aria-expanded': boolean;
7
+ 'aria-controls': string;
8
+ onclick: (event: MouseEvent) => void;
9
+ }
6
10
  export interface PopoverProps extends Component {
7
11
  ref?: HTMLElement | null;
8
12
  open?: boolean;
9
13
  rounded?: RoundedType | 'full';
10
14
  position?: 'top' | 'bottom' | 'left' | 'right';
11
15
  color?: ColorProps;
12
- activator?: Snippet<[ModelPopoverProps]>;
16
+ trigger?: Snippet<[PopoverTriggerProps]>;
13
17
  elevation?: ElevationProps;
14
18
  density?: 'dense' | 'compact' | 'default' | 'comfortable' | 'spacious';
19
+ /** removed in 0.8.0: use the trigger snippet */
20
+ activator?: never;
15
21
  }
@@ -6,8 +6,7 @@
6
6
  ref = $bindable(),
7
7
  value = $bindable(),
8
8
  type = 'text',
9
- min,
10
- max,
9
+ maxlength,
11
10
  counter = false,
12
11
  hideSpinButtons = false,
13
12
  class: className,
@@ -21,6 +20,7 @@
21
20
  rounded,
22
21
  color,
23
22
  elevation,
23
+ label,
24
24
  disabled = false,
25
25
  readonly = false,
26
26
  error = false,
@@ -44,13 +44,6 @@
44
44
  let inputRef = $state<HTMLInputElement | null>(null);
45
45
  let text = $derived(`${value ?? ''}`);
46
46
 
47
- $effect(() => {
48
- if (typeof max === 'number' && max > 0 && text.length > max) {
49
- const truncated = text.slice(0, max);
50
- value = typeof value === 'number' ? Number(truncated) || 0 : truncated;
51
- }
52
- });
53
-
54
47
  function clear() {
55
48
  value = '';
56
49
  inputRef?.focus();
@@ -70,6 +63,7 @@
70
63
  {rounded}
71
64
  {color}
72
65
  {elevation}
66
+ {label}
73
67
  {disabled}
74
68
  {readonly}
75
69
  {error}
@@ -88,7 +82,7 @@
88
82
  {prependInner}
89
83
  {appendInner}
90
84
  dirty={text.length > 0}
91
- counter={counter ? `${text.length}${max ? `/${max}` : ''}` : undefined}
85
+ counter={counter ? `${text.length}${maxlength ? `/${maxlength}` : ''}` : undefined}
92
86
  onclear={clear}
93
87
  data-hide-spin-buttons={type === 'number' && hideSpinButtons}
94
88
  >
@@ -101,8 +95,7 @@
101
95
  class="kit-textfield__input"
102
96
  {type}
103
97
  size="1"
104
- minlength={min}
105
- maxlength={max}
98
+ {maxlength}
106
99
  />
107
100
  {/snippet}
108
101
  </Field>
@@ -1,7 +1,7 @@
1
1
  import type { HTMLInputAttributes } from 'svelte/elements';
2
2
  import type { ClassNameType, KitClassNameType, KitStylePropertiesType, StylePropertiesType } from '../../@types';
3
3
  import type { FieldBaseProps } from '../field/field.types';
4
- type FieldOwnedAttributes = keyof FieldBaseProps | 'size' | 'type' | 'value' | 'min' | 'max';
4
+ type FieldOwnedAttributes = keyof FieldBaseProps | 'size' | 'type' | 'value';
5
5
  export interface TextfieldProps extends FieldBaseProps, Omit<HTMLInputAttributes, FieldOwnedAttributes | 'class' | 'style' | 'children'> {
6
6
  ref?: HTMLElement | null;
7
7
  class?: ClassNameType;
@@ -10,10 +10,6 @@ export interface TextfieldProps extends FieldBaseProps, Omit<HTMLInputAttributes
10
10
  's-style'?: KitStylePropertiesType;
11
11
  value?: string | number;
12
12
  type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url';
13
- /** minimal length of the value */
14
- min?: number;
15
- /** maximal length of the value, also used by the counter */
16
- max?: number;
17
13
  counter?: boolean;
18
14
  hideSpinButtons?: boolean;
19
15
  }
@@ -0,0 +1,9 @@
1
+ import type { TooltipValue } from './tooltip.types.ts';
2
+ /**
3
+ * tooltip - Shows the shared tooltip on hover and keyboard focus of the node.
4
+ * A node without an accessible name gets the label as aria-label, the others get it as a description.
5
+ */
6
+ export declare function tooltip(node: HTMLElement, value: TooltipValue): {
7
+ update(next: TooltipValue): void;
8
+ destroy(): void;
9
+ };
@@ -0,0 +1,159 @@
1
+ import { mount } from 'svelte';
2
+ const sharedId = 'kit-tooltip-shared';
3
+ // one tooltip for every kit:btn, kit:chip and kit:icon, mounted on the first hover or focus
4
+ const shared = $state({
5
+ id: sharedId,
6
+ open: false,
7
+ label: '',
8
+ location: 'bottom',
9
+ variant: undefined,
10
+ density: 'default',
11
+ rounded: 'xs',
12
+ delayDuration: 850,
13
+ closeDelay: 100
14
+ });
15
+ let loading = null;
16
+ let owner = null;
17
+ function load() {
18
+ loading ??= import('./tooltip.svelte').then(({ default: Tooltip }) => {
19
+ const instance = mount(Tooltip, { target: document.body, props: shared });
20
+ return instance.triggerHandlers;
21
+ });
22
+ return loading;
23
+ }
24
+ function normalize(value) {
25
+ if (!value)
26
+ return null;
27
+ return typeof value === 'string' ? { label: value } : value.label ? value : null;
28
+ }
29
+ /**
30
+ * tooltip - Shows the shared tooltip on hover and keyboard focus of the node.
31
+ * A node without an accessible name gets the label as aria-label, the others get it as a description.
32
+ */
33
+ export function tooltip(node, value) {
34
+ let options = normalize(value);
35
+ let named = false;
36
+ let hovered = false;
37
+ let listening = false;
38
+ function syncName() {
39
+ if (named)
40
+ node.removeAttribute('aria-label');
41
+ named = false;
42
+ if (!options)
43
+ return;
44
+ const hasName = node.hasAttribute('aria-label') ||
45
+ node.hasAttribute('aria-labelledby') ||
46
+ !!node.textContent?.trim();
47
+ if (hasName)
48
+ return;
49
+ node.setAttribute('aria-label', options.label);
50
+ named = true;
51
+ }
52
+ function describe(on) {
53
+ if (named)
54
+ return;
55
+ const ids = (node.getAttribute('aria-describedby') ?? '').split(' ').filter(Boolean);
56
+ const others = ids.filter((id) => id !== sharedId);
57
+ const next = on ? [...others, sharedId] : others;
58
+ if (next.length)
59
+ node.setAttribute('aria-describedby', next.join(' '));
60
+ else
61
+ node.removeAttribute('aria-describedby');
62
+ }
63
+ function activate() {
64
+ if (!options)
65
+ return;
66
+ owner = node;
67
+ Object.assign(shared, {
68
+ label: options.label,
69
+ location: options.location ?? 'bottom',
70
+ variant: options.variant,
71
+ density: options.density ?? 'default',
72
+ rounded: options.rounded ?? 'xs',
73
+ delayDuration: options.delayDuration ?? 850,
74
+ closeDelay: options.closeDelay ?? 100
75
+ });
76
+ }
77
+ // the handlers read currentTarget and target, gone once the event is dispatched
78
+ const as = (event, target = event.target) => ({ currentTarget: node, target, pointerType: event.pointerType });
79
+ const onPointerEnter = (event) => {
80
+ hovered = true;
81
+ if (event.pointerType === 'touch')
82
+ return;
83
+ const fake = as(event);
84
+ load().then((handlers) => {
85
+ if (!hovered)
86
+ return;
87
+ activate();
88
+ handlers().onpointerenter(fake);
89
+ });
90
+ };
91
+ const onPointerLeave = (event) => {
92
+ hovered = false;
93
+ if (owner === node)
94
+ loading?.then((handlers) => handlers().onpointerleave(event));
95
+ };
96
+ const onPointerDown = (event) => {
97
+ if (owner === node)
98
+ loading?.then((handlers) => handlers().onpointerdown(event));
99
+ };
100
+ const onFocusIn = (event) => {
101
+ if (node.contains(event.relatedTarget))
102
+ return;
103
+ describe(true);
104
+ const fake = as(event);
105
+ load().then((handlers) => {
106
+ if (!node.contains(document.activeElement))
107
+ return;
108
+ activate();
109
+ handlers().onfocus(fake);
110
+ });
111
+ };
112
+ const onFocusOut = (event) => {
113
+ if (node.contains(event.relatedTarget))
114
+ return;
115
+ describe(false);
116
+ if (owner === node)
117
+ loading?.then((handlers) => handlers().onblur(event));
118
+ };
119
+ function listen(on) {
120
+ if (on === listening)
121
+ return;
122
+ listening = on;
123
+ const method = on ? 'addEventListener' : 'removeEventListener';
124
+ node[method]('pointerenter', onPointerEnter);
125
+ node[method]('pointerleave', onPointerLeave);
126
+ node[method]('pointerdown', onPointerDown);
127
+ node[method]('focusin', onFocusIn);
128
+ node[method]('focusout', onFocusOut);
129
+ }
130
+ function release() {
131
+ if (owner !== node)
132
+ return;
133
+ owner = null;
134
+ shared.open = false;
135
+ }
136
+ listen(!!options);
137
+ syncName();
138
+ return {
139
+ update(next) {
140
+ options = normalize(next);
141
+ listen(!!options);
142
+ syncName();
143
+ if (!options) {
144
+ describe(false);
145
+ release();
146
+ }
147
+ else if (owner === node) {
148
+ activate();
149
+ }
150
+ },
151
+ destroy() {
152
+ listen(false);
153
+ if (named)
154
+ node.removeAttribute('aria-label');
155
+ describe(false);
156
+ release();
157
+ }
158
+ };
159
+ }