Skip to content

Commit

Permalink
feature: add animation to popover components (#974)
Browse files Browse the repository at this point in the history
* feat(popover): add animation control to Popover Canvas and Popover Menu stories

* feat(popover): add animation prop to Popover components for open/close effects

* feat(utils): add generateClassList function for dynamic class name generation

* feat(popover): add fade animations for popover visibility transitions

* feat(popover): add animation property to control opening/closing animations

* feat(popover): change animation control from select to radio for Popover Canvas and Menu stories

* refactor(popover): clean up fade animation styles in SCSS
  • Loading branch information
ckrook authored Jan 21, 2025
1 parent 38afcbe commit 5066fe4
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,25 @@ export default {
defaultValue: { summary: 'auto' },
},
},
animation: {
name: 'Animation',
description: 'Sets the animation of the Popover Canvas.',
control: {
type: 'radio',
},
options: ['none', 'fade'],
table: {
defaultValue: { summary: 'none' },
},
},
},
args: {
canvasPosition: 'Auto',
animation: 'none',
},
};

const ComponentPopoverCanvas = ({ canvasPosition }) => {
const ComponentPopoverCanvas = ({ canvasPosition, animation }) => {
const canvasPosLookup = {
'Bottom': 'bottom',
'Bottom start': 'bottom-start',
Expand Down Expand Up @@ -78,6 +90,7 @@ const ComponentPopoverCanvas = ({ canvasPosition }) => {
<tds-popover-canvas
id="my-popover-canvas"
placement="${canvasPosLookup[canvasPosition]}"
animation="${animation}"
selector="#trigger"
class="tds-u-p2">
<h2 class="tds-headline-02 tds-u-mt0">A Popover Canvas!</h2>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export class TdsPopoverCanvas {
/** Sets the offset skidding */
@Prop() offsetSkidding: number = 0;

/** Whether the popover should animate when being opened/closed or not */
@Prop() animation: 'none' | 'fade' | string = 'none';

/** Sets the offset distance */
@Prop() offsetDistance: number = 8;

Expand Down Expand Up @@ -76,6 +79,7 @@ export class TdsPopoverCanvas {
this.childRef = el;
}}
defaultShow={this.defaultShow}
animation={this.animation}
>
<div>
{/* (@stencil/[email protected]): This div is somehow needed to keep the slotted children in a predictable order */}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/popover-canvas/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Example:

| Property | Attribute | Description | Type | Default |
| ---------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| `animation` | `animation` | Whether the popover should animate when being opened/closed or not | `string` | `'none'` |
| `defaultShow` | `default-show` | Decides if the component should be visible from the start. | `boolean` | `false` |
| `modifiers` | -- | Array of modifier objects to pass to popper.js. See https://popper.js.org/docs/v2/modifiers/ | `Object[]` | `[]` |
| `offsetDistance` | `offset-distance` | Sets the offset distance | `number` | `8` |
Expand Down
28 changes: 22 additions & 6 deletions packages/core/src/components/popover-core/popover-core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import {
import { createPopper } from '@popperjs/core';
import type { Placement, Instance } from '@popperjs/core';
import generateUniqueId from '../../utils/generateUniqueId';
import { generateClassList } from '../../utils/classList';

@Component({
tag: 'tds-popover-core',
styleUrl: 'tds-popover-core.scss',
shadow: false,
scoped: true,
})
Expand All @@ -32,6 +34,9 @@ export class TdsPopoverCore {
/** Decides if the component should be visible from the start. */
@Prop() defaultShow: boolean = false;

/** Whether the popover should animate when being opened/closed or not */
@Prop() animation: 'none' | 'fade' | string = 'none';

/** Controls whether the Popover is shown or not. If this is set hiding and showing
* will be decided by this prop and will need to be controlled from the outside. This
* also means that clicking outside of the popover won't close it. Takes precedence over `defaultShow` prop.
Expand Down Expand Up @@ -67,6 +72,8 @@ export class TdsPopoverCore {

@State() disableLogic: boolean = false;

@State() hasShownAtLeastOnce: boolean = false;

/** Property for closing popover programmatically */
@Method() async close() {
this.setIsShown(false);
Expand Down Expand Up @@ -141,6 +148,7 @@ export class TdsPopoverCore {
this.isShown = isShown;
}
if (this.isShown) {
this.hasShownAtLeastOnce = true;
this.internalTdsShow.emit();
} else {
this.internalTdsClose.emit();
Expand Down Expand Up @@ -245,10 +253,13 @@ export class TdsPopoverCore {
});
}

/* To enable initial loading of a component if user controls show prop*/
/* To enable initial loading of a component if user controls show prop */
componentWillLoad() {
// Ensure initial visibility is handled properly
if (this.show === true || this.defaultShow === true) {
this.setIsShown(true);
} else {
this.setIsShown(false);
}
}

Expand All @@ -266,13 +277,18 @@ export class TdsPopoverCore {
}

render() {
let hostStyle = {};
if (this.autoHide) {
hostStyle = { display: this.isShown ? 'block' : 'none' };
}
const classes = {
'is-shown': (this.isShown && this.animation === 'none') || this.animation === undefined,
'is-hidden': (!this.isShown && this.animation === 'none') || this.animation === undefined,
'initially-hidden': !this.hasShownAtLeastOnce,
'tds-animation-enter-fade': this.isShown && this.animation === 'fade',
'tds-animation-exit-fade': !this.isShown && this.animation === 'fade',
};

const classList = generateClassList(classes);

return (
<Host style={hostStyle} id={`tds-popover-core-${this.uuid}`}>
<Host class={classList} id={`tds-popover-core-${this.uuid}`}>
<slot></slot>
</Host>
);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/popover-core/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

| Property | Attribute | Description | Type | Default |
| ---------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| `animation` | `animation` | Whether the popover should animate when being opened/closed or not | `string` | `'none'` |
| `autoHide` | `auto-hide` | Decides if the popover should hide automatically. Alternatevly it can be hidden externally based on emitted events. | `boolean` | `true` |
| `defaultShow` | `default-show` | Decides if the component should be visible from the start. | `boolean` | `false` |
| `modifiers` | -- | Array of modifier objects to pass to popper.js. See https://popper.js.org/docs/v2/modifiers/ | `Object[]` | `[]` |
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/components/popover-core/tds-popover-core.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@import '../../global/global';

:host {
pointer-events: none;
display: block;
}

:host(.tds-animation-enter-fade) {
animation: tds-fade-in var(--tds-motion-duration-moderate-01) var(--tds-motion-easing-enter)
forwards;
}

:host(.tds-animation-exit-fade) {
animation: tds-fade-out var(--tds-motion-duration-moderate-01) var(--tds-motion-easing-exit)
forwards;
}

:host(.is-shown) {
opacity: 1;
pointer-events: auto;
visibility: visible;
}

:host(.is-hidden) {
opacity: 0;
pointer-events: none;
visibility: hidden;
}

:host(.initially-hidden) {
opacity: 0;
pointer-events: none;
visibility: hidden;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
width: 100%;
display: flex;
align-items: center;
transition: background-color var(--tds-motion-duration-fast-02) var(--tds-motion-easing-easy);

&:hover {
cursor: pointer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,27 @@ export default {
type: 'boolean',
},
},
animation: {
name: 'Animation',
description: 'Sets the animation of the Popover Menu.',
control: {
type: 'radio',
},
options: ['none', 'fade'],
table: {
defaultValue: { summary: 'none' },
},
},
},
args: {
menuPosition: 'Auto',
icons: false,
fluidWidth: false,
animation: 'none',
},
};

const Template = ({ menuPosition, icons, fluidWidth }) => {
const Template = ({ menuPosition, icons, fluidWidth, animation }) => {
const menuPosLookup = {
'Bottom': 'bottom',
'Bottom start': 'bottom-start',
Expand Down Expand Up @@ -101,6 +113,7 @@ const Template = ({ menuPosition, icons, fluidWidth }) => {
<tds-popover-menu
id="my-popover-menu"
placement="${menuPosLookup[menuPosition]}"
animation="${animation}"
${fluidWidth ? 'fluid-width' : ''}
selector="#my-popover-button"
>
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/components/popover-menu/popover-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export class TdsPopoverMenu {
/** Decides the placement of the Popover Menu */
@Prop() placement: Placement = 'auto';

/** Whether the popover should animate when being opened/closed or not */
@Prop() animation: 'none' | 'fade' | string = 'none';

/** Sets the offset skidding */
@Prop() offsetSkidding: number = 0;

Expand Down Expand Up @@ -74,6 +77,7 @@ export class TdsPopoverMenu {
this.childRef = el;
}}
defaultShow={this.defaultShow}
animation={this.animation}
>
<div role="list">
<slot></slot>
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/popover-menu/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Example:

| Property | Attribute | Description | Type | Default |
| ---------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| `animation` | `animation` | Whether the popover should animate when being opened/closed or not | `string` | `'none'` |
| `defaultShow` | `default-show` | Decides if the component should be visible from the start. | `boolean` | `false` |
| `fluidWidth` | `fluid-width` | If true this unsets the width (160px) of the Popover Menu | `boolean` | `false` |
| `offsetDistance` | `offset-distance` | Sets the offset distance | `number` | `8` |
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/utils/classList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Generates a string of class names by filtering out keys from the `classes` object
* whose values evaluate to `false`. The keys that remain will be joined with a space.
*
* @param classes - An object where keys represent class names and values are boolean
* flags indicating whether to include the class name.
* @returns A string of class names separated by spaces.
*
* @example
* const classes = {
* 'active': true,
* 'disabled': false,
* 'highlighted': true,
* };
* const classList = generateClassList(classes);
* console.log(classList); // Output: "active highlighted"
*/
export const generateClassList = (classes: Record<string, boolean>): string => {
return Object.keys(classes)
.filter((key) => classes[key])
.join(' ');
};

0 comments on commit 5066fe4

Please sign in to comment.